Merge "Remove user-rescoping from PermissionBackend"
diff --git a/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch b/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch
deleted file mode 100644
index 3ccf5cd..0000000
--- a/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch
+++ /dev/null
@@ -1,133 +0,0 @@
-Date: Wed, 30 May 2018 21:22:18 +0200
-Subject: [PATCH] Replace native {http,git}_archive with Skylark rules
-
-See [1] for more details.
-
-Test Plan:
-
-* Apply this CL on Bazel master: [2] and build bazel
-* Run with this custom built bazel version:
-
-  $ bazel test //javatests/...
-  $ bazel test //closure/...
-
-[1] https://groups.google.com/d/topic/bazel-discuss/dO2MHQLwJF0/discussion
-[2] https://bazel-review.googlesource.com/#/c/bazel/+/55932/
----
- closure/repositories.bzl | 23 ++++++++++++-----------
- 1 file changed, 12 insertions(+), 11 deletions(-)
-
-diff --git a/closure/repositories.bzl b/closure/repositories.bzl
-index 9b84a72..2816fb6 100644
---- closure/repositories.bzl
-+++ closure/repositories.bzl
-@@ -14,6 +14,7 @@
- 
- """External dependencies for Closure Rules."""
- 
-+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
- load("//closure/private:java_import_external.bzl", "java_import_external")
- load("//closure/private:platform_http_file.bzl", "platform_http_file")
- load("//closure:filegroup_external.bzl", "filegroup_external")
-@@ -405,7 +406,7 @@ def com_google_common_html_types():
-   )
- 
- def com_google_common_html_types_html_proto():
--  native.http_file(
-+  http_file(
-       name = "com_google_common_html_types_html_proto",
-       sha256 = "6ece202f11574e37d0c31d9cf2e9e11a0dbc9218766d50d211059ebd495b49c3",
-       urls = [
-@@ -633,7 +634,7 @@ def com_google_javascript_closure_compiler():
- 
- def com_google_javascript_closure_library():
-   # After updating: bazel run //closure/library:regenerate -- "$PWD"
--  native.new_http_archive(
-+  http_archive(
-       name = "com_google_javascript_closure_library",
-       urls = [
-           "https://mirror.bazel.build/github.com/google/closure-library/archive/v20180405.tar.gz",
-@@ -658,7 +659,7 @@ def com_google_jsinterop_annotations():
- 
- def com_google_protobuf():
-   # Note: Protobuf 3.6.0+ is going to use C++11
--  native.http_archive(
-+  http_archive(
-       name = "com_google_protobuf",
-       strip_prefix = "protobuf-3.5.1",
-       sha256 = "826425182ee43990731217b917c5c3ea7190cfda141af4869e6d4ad9085a740f",
-@@ -669,7 +670,7 @@ def com_google_protobuf():
-   )
- 
- def com_google_protobuf_js():
--  native.new_http_archive(
-+  http_archive(
-       name = "com_google_protobuf_js",
-       urls = [
-           "https://mirror.bazel.build/github.com/google/protobuf/archive/v3.5.1.tar.gz",
-@@ -722,7 +723,7 @@ def com_google_template_soy():
-   )
- 
- def com_google_template_soy_jssrc():
--  native.new_http_archive(
-+  http_archive(
-       name = "com_google_template_soy_jssrc",
-       sha256 = "c76ab4cb6e46a7c76336640b3c40d6897b420209a6c0905cdcd32533dda8126a",
-       urls = [
-@@ -757,7 +758,7 @@ def com_squareup_javapoet():
-   )
- 
- def fonts_noto_hinted_deb():
--  native.http_file(
-+  http_file(
-       name = "fonts_noto_hinted_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fonts-noto/fonts-noto-hinted_20161116-1_all.deb",
-@@ -767,7 +768,7 @@ def fonts_noto_hinted_deb():
-   )
- 
- def fonts_noto_mono_deb():
--  native.http_file(
-+  http_file(
-       name = "fonts_noto_mono_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fonts-noto/fonts-noto-mono_20161116-1_all.deb",
-@@ -801,7 +802,7 @@ def javax_inject():
-   )
- 
- def libexpat_amd64_deb():
--  native.http_file(
-+  http_file(
-       name = "libexpat_amd64_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/e/expat/libexpat1_2.1.0-6+deb8u3_amd64.deb",
-@@ -811,7 +812,7 @@ def libexpat_amd64_deb():
-   )
- 
- def libfontconfig_amd64_deb():
--  native.http_file(
-+  http_file(
-       name = "libfontconfig_amd64_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fontconfig/libfontconfig1_2.11.0-6.3+deb8u1_amd64.deb",
-@@ -821,7 +822,7 @@ def libfontconfig_amd64_deb():
-   )
- 
- def libfreetype_amd64_deb():
--  native.http_file(
-+  http_file(
-       name = "libfreetype_amd64_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/freetype/libfreetype6_2.5.2-3+deb8u1_amd64.deb",
-@@ -831,7 +832,7 @@ def libfreetype_amd64_deb():
-   )
- 
- def libpng_amd64_deb():
--  native.http_file(
-+  http_file(
-       name = "libpng_amd64_deb",
-       urls = [
-           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/libp/libpng/libpng12-0_1.2.50-2+deb8u2_amd64.deb",
--- 
-2.16.3
-
diff --git a/0002-Bump-Dagger-to-2.14.1-to-support-Java-9.patch b/0002-Bump-Dagger-to-2.14.1-to-support-Java-9.patch
deleted file mode 100644
index df6780d..0000000
--- a/0002-Bump-Dagger-to-2.14.1-to-support-Java-9.patch
+++ /dev/null
@@ -1,116 +0,0 @@
-Date: Thu, 21 Jun 2018 19:29:13 +0200
-Subject: [PATCH 2/2] Bump Dagger to 2.14.1 to support Java 9
-
-Closes #275
----
- closure/repositories.bzl | 41 +++++++++++++++++++++++++++++------------
- 1 file changed, 29 insertions(+), 12 deletions(-)
-
-diff --git a/closure/repositories.bzl b/closure/repositories.bzl
-index 2816fb6..fe63b6f 100644
---- closure/repositories.bzl
-+++ closure/repositories.bzl
-@@ -35,6 +35,7 @@ def closure_repositories(
-     omit_com_google_dagger=False,
-     omit_com_google_dagger_compiler=False,
-     omit_com_google_dagger_producers=False,
-+    omit_com_google_dagger_spi=False,
-     omit_com_google_errorprone_error_prone_annotations=False,
-     omit_com_google_errorprone_javac_shaded=False,
-     omit_com_google_guava=False,
-@@ -101,6 +102,8 @@ def closure_repositories(
-     com_google_dagger_compiler()
-   if not omit_com_google_dagger_producers:
-     com_google_dagger_producers()
-+  if not omit_com_google_dagger_spi:
-+    com_google_dagger_spi()
-   if not omit_com_google_errorprone_error_prone_annotations:
-     com_google_errorprone_error_prone_annotations()
-   if not omit_com_google_errorprone_javac_shaded:
-@@ -418,10 +421,10 @@ def com_google_common_html_types_html_proto():
- def com_google_dagger():
-   java_import_external(
-       name = "com_google_dagger",
--      jar_sha256 = "8b7806518bed270950002158934fbd8281725ee09909442f2f22b58520b667a7",
-+      jar_sha256 = "374cfee26c9c93f44caa1946583c9edc135bb9a42838476522551ec46aa55c7c",
-       jar_urls = [
--          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger/2.9/dagger-2.9.jar",
--          "https://repo1.maven.org/maven2/com/google/dagger/dagger/2.9/dagger-2.9.jar",
-+          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger/2.14.1/dagger-2.14.1.jar",
-+          "https://repo1.maven.org/maven2/com/google/dagger/dagger/2.14.1/dagger-2.14.1.jar",
-       ],
-       licenses = ["notice"],  # Apache 2.0
-       deps = ["@javax_inject"],
-@@ -441,17 +444,20 @@ def com_google_dagger():
- def com_google_dagger_compiler():
-   java_import_external(
-       name = "com_google_dagger_compiler",
--      jar_sha256 = "afe356def27710db5b60cad8e7a6c06510dc3d3b854f30397749cbf0d0e71315",
-+      jar_sha256 = "ff16d55273e375349537fc82292b00de04d8a2caca2d4aa6c642692b1a68194d",
-       jar_urls = [
--          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.9/dagger-compiler-2.9.jar",
--          "https://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.9/dagger-compiler-2.9.jar",
-+          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.14.1/dagger-compiler-2.14.1.jar",
-+          "https://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.14.1/dagger-compiler-2.14.1.jar",
-       ],
-       licenses = ["notice"],  # Apache 2.0
-       deps = [
-           "@com_google_code_findbugs_jsr305",
-           "@com_google_dagger//:runtime",
-           "@com_google_dagger_producers//:runtime",
-+          "@com_google_dagger_spi",
-           "@com_google_guava",
-+          "@com_google_java_format",
-+          "@com_squareup_javapoet",
-       ],
-       extra_build_file_content = "\n".join([
-           "java_plugin(",
-@@ -471,10 +477,10 @@ def com_google_dagger_compiler():
- def com_google_dagger_producers():
-   java_import_external(
-       name = "com_google_dagger_producers",
--      jar_sha256 = "b452dc1b95dd02f6272e97b15d1bd35d92b5f484a7d69bb73887b6c6699d8843",
-+      jar_sha256 = "96f950bc4b94d013b0c538632a4bc630f33eda8b01f63ae752b76c5e48783859",
-       jar_urls = [
--          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.9/dagger-producers-2.9.jar",
--          "https://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.9/dagger-producers-2.9.jar",
-+          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.14.1/dagger-producers-2.14.1.jar",
-+          "https://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.14.1/dagger-producers-2.14.1.jar",
-       ],
-       licenses = ["notice"],  # Apache 2.0
-       deps = [
-@@ -495,6 +501,17 @@ def com_google_dagger_producers():
-       ]),
-   )
- 
-+def com_google_dagger_spi():
-+  java_import_external(
-+      name = "com_google_dagger_spi",
-+      jar_sha256 = "6a20d6c6620fefe50747e9e910e0d0c178cf39d76b67ccffb505ac9a167302cb",
-+      jar_urls = [
-+          "https://mirror.bazel.build/repo1.maven.org/maven2/com/google/dagger/dagger-spi/2.14.1/dagger-spi-2.14.1.jar",
-+          "https://repo1.maven.org/maven2/com/google/dagger/dagger-spi/2.14.1/dagger-spi-2.14.1.jar",
-+      ],
-+      licenses = ["notice"],  # Apache 2.0
-+  )
-+
- def com_google_errorprone_error_prone_annotations():
-   java_import_external(
-       name = "com_google_errorprone_error_prone_annotations",
-@@ -749,10 +766,10 @@ def com_ibm_icu_icu4j():
- def com_squareup_javapoet():
-   java_import_external(
-       name = "com_squareup_javapoet",
--      jar_sha256 = "2f671d5f056f04922feff78dd60c34979fc9863b16ad706551a9b68842c1a3d0",
-+      jar_sha256 = "5bb5abdfe4366c15c0da3332c57d484e238bd48260d6f9d6acf2b08fdde1efea",
-       jar_urls = [
--          "https://mirror.bazel.build/repo1.maven.org/maven2/com/squareup/javapoet/1.7.0/javapoet-1.7.0.jar",
--          "https://repo1.maven.org/maven2/com/squareup/javapoet/1.7.0/javapoet-1.7.0.jar",
-+          "https://mirror.bazel.build/repo1.maven.org/maven2/com/squareup/javapoet/1.9.0/javapoet-1.9.0.jar",
-+          "https://repo1.maven.org/maven2/com/squareup/javapoet/1.9.0/javapoet-1.9.0.jar",
-       ],
-       licenses = ["notice"],  # Apache 2.0
-   )
--- 
-2.16.3
-
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 3b2b65f..cad9489 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -217,6 +217,19 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[change.workInProgressByDefault]]change.workInProgressByDefault::
++
+Controls whether all new changes in the project are set as WIP by default.
++
+Note that a new change will be ready if the `workInProgress` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `ready` link:user-upload.html#wip[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 04f749d..8b4bece 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -417,6 +417,92 @@
 details. Users should watch the cache sizes and clean them manually if
 necessary.
 
+[[npm-binary]]
+== NPM Binaries
+
+Parts of the PolyGerrit build require running NPM-based JavaScript programs as
+"binaries". We don't attempt to resolve and download NPM dependencies at build
+time, but instead use pre-built bundles of the NPM binary along with all its
+dependencies. Some packages on
+link:https://docs.npmjs.com/misc/registry[registry.npmjs.org] come with their
+dependencies bundled, but this is the exception rather than the rule. More
+commonly, to add a new binary to this list, you will need to bundle the binary
+yourself.
+
+[NOTE]
+We can only use binaries that meet certain licensing requirements, and that do
+not include any native code.
+
+Start by checking that the license and file types of the bundle are acceptable:
+[source,bash]
+----
+  gerrit_repo=/path/to/gerrit
+  package=some-npm-package
+  version=1.2.3
+
+  npm install -g license-checker && \
+  rm -rf /tmp/$package-$version && mkdir -p /tmp/$package-$version && \
+  cd /tmp/$package-$version && \
+  npm install $package@$version && \
+  license-checker | grep licenses: | sort -u
+----
+
+This will output a list of the different licenses used by the package and all
+its transitive dependencies. We can only legally distribute a bundle via our
+storage bucket if the licenses allow us to do so. As long as all of the listed
+license are allowed by
+link:https://opensource.google.com/docs/thirdparty/licenses/[Google's
+standards]. Any `by_exception_only`, commercial, prohibited, or unlisted
+licenses are not allowed; otherwise, it is ok to distribute the source. If in
+doubt, contact a maintainer who is a Googler.
+
+Next, check the file types:
+[source,bash]
+----
+  cd /tmp/$package-$version
+  find . -type f | xargs file | grep -v 'ASCII\|UTF-8\|empty$'
+----
+
+If you see anything that looks like a native library or binary, then we can't
+use the bundle.
+
+If everything looks good, create the bundle, and note the SHA-1:
+[source,bash]
+----
+  $gerrit_repo/tools/js/npm_pack.py $package $version && \
+  sha1sum $package-$version.tgz
+----
+
+This creates a file named `$package-$version.tgz` in your working directory.
+
+Any project maintainer can upload this file to the
+link:https://console.cloud.google.com/storage/browser/gerrit-maven/npm-packages[storage
+bucket].
+
+Finally, add the new binary to the build process:
+----
+  # WORKSPACE
+  npm_binary(
+      name = "some-npm-package",
+      repository = GERRIT,
+  )
+
+  # lib/js/npm.bzl
+  NPM_VERSIONS = {
+    ...
+    "some-npm-package": "1.2.3",
+  }
+
+  NPM_SHA1S = {
+    ...
+    "some-npm-package": "<sha1>",
+  }
+----
+
+To use the binary from the Bazel build, you need to use the `run_npm_binary.py`
+wrapper script. For an example, see the use of `crisper` in `tools/bzl/js.bzl`.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index e065e57..62d73cc 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,7 +164,7 @@
 
 To format Java source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.5), and to format Bazel BUILD and WORKSPACE files the
+tool (version 1.5), and to format Bazel BUILD, WORKSPACE and .bzl files the
 link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
 tool (version 0.12.0).
 These tools automatically apply format according to the style guides; this
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 35ff40b..5c008c7 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2734,7 +2734,7 @@
 [source, java]
 ----
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
 
 public class MyPlugin implements MailFilter {
   public boolean shouldProcessMessage(MailMessage message) {
@@ -2871,6 +2871,17 @@
 Plugin authors should also consider binding their SubmitRule using a `Gerrit-BatchModule`.
 See link:dev-plugins.html[Batch runtime] for more informations.
 
+
+The SubmitRule extension point allows you to write complex rules, but writing
+small self-contained rules should be preferred: doing so allows end users to
+compose several rules to form more complex submit checks.
+
+The `SubmitRequirement` class allows rules to communicate what the user needs
+to change in order to be compliant. These requirements should be kept once they
+are met, but marked as `OK`. If the requirements were not displayed, reviewers
+would need to use their precious time to manually check that they were met.
+
+
 == SEE ALSO
 
 * link:js-api.html[JavaScript API]
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 7b80229..2503449 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -172,7 +172,14 @@
   user@host:~/demo-project$
 ----
 
-Then make a change to it and upload it as a reviewable change in Gerrit.
+Install the link:user-changeid.html[Change-Id commitmsg hook]
+
+----
+  scp -p -P 29418 user@localhost:hooks/commit-msg $(git rev-parse --git-dir)/hooks/
+----
+
+Then make a change to the repository and upload it as a reviewable change
+in Gerrit.
 
 ----
   user@host:~/demo-project$ date > testfile.txt
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 436408d..662e0b3 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -827,6 +827,12 @@
 and download commands. Note that this option is only shown if the Flash plugin
 is available and the JavaScript Clipboard API is unavailable.
 
+- [[work-in-progress-by-default]]`Set new changes work-in-progress`:
++
+Whether new changes are uploaded as work-in-progress per default. This
+preference just sets the default; the behavior can still be overridden using a
+link:user-upload.html#wip[push option].
+
 [[my-menu]]
 In addition it is possible to customize the menu entries of the `My`
 menu. This can be used to make the navigation to frequently used
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 025b29d..edb642e 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1255,6 +1255,7 @@
     "review_category_strategy": "ABBREV",
     "mute_common_path_prefixes": true,
     "publish_comments_on_push": true,
+    "work_in_progress_by_default": true,
     "default_base_for_merges": "FIRST_PARENT",
     "my": [
       {
@@ -1361,6 +1362,7 @@
     "review_category_strategy": "NAME",
     "diff_view": "SIDE_BY_SIDE",
     "publish_comments_on_push": true,
+    "work_in_progress_by_default": true,
     "mute_common_path_prefixes": true,
     "my": [
       {
@@ -2654,6 +2656,9 @@
 |`publish_comments_on_push`     |not set if `false`|
 Whether to link:user-upload.html#publish-comments[publish draft comments] on
 push by default.
+|`work_in_progress_by_default`  |not set if `false`|
+Whether to link:user-upload.html#wip[set work-in-progress] on
+push or on create changes online by default.
 |============================================
 
 [[preferences-input]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fb0a0df..3b25f47 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2277,7 +2277,9 @@
 --
 
 Marks a change as ignored. The change will not be shown in the incoming
-reviews dashboard, and email notifications will be suppressed.
+reviews dashboard, and email notifications will be suppressed. Ignoring
+a change does not cause the change's "updated" timestamp to be modified,
+and the owner is not notified.
 
 .Request
 ----
@@ -6282,10 +6284,14 @@
 Only set if the file was renamed or copied.
 |`lines_inserted`|optional|
 Number of inserted lines. +
-Not set for binary files or if no lines were inserted.
+Not set for binary files or if no lines were inserted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
 |`lines_deleted` |optional|
 Number of deleted lines. +
-Not set for binary files or if no lines were deleted.
+Not set for binary files or if no lines were deleted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
 |`size_delta`    ||
 Number of bytes by which the file size increased/decreased.
 |`size`          ||
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 792cca8..4a5cd1d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2890,10 +2890,13 @@
 |`reject_implicit_merges`|optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 implicit merges should be rejected on changes pushed to the project.
-|`private_by_default`     ||
+|`private_by_default`         ||
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 all new changes are set as private by default.
-|`max_object_size_limit`     ||
+|`work_in_progress_by_default`||
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+all new changes are set as work-in-progress by default.
+|`max_object_size_limit`      ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
 MaxObjectSizeLimitInfo] entity.
diff --git a/Documentation/user-search-projects.txt b/Documentation/user-search-projects.txt
index ba20adb..11c1326 100644
--- a/Documentation/user-search-projects.txt
+++ b/Documentation/user-search-projects.txt
@@ -24,6 +24,11 @@
 Matches projects whose description contains 'DESCRIPTION', using a
 full-text search.
 
+[[state]]
+state:'STATE'::
++
+Matches project's state. Can be either 'active' or 'read-only'.
+
 == Magical Operators
 
 [[is-visible]]
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index ce62b93..baf388e 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -309,6 +309,11 @@
 Only change owners, project owners and site administrators can specify
 `work-in-progress` and `ready` options on push.
 
+The default for this option can be set as a
+link:intro-user.html#work-in-progress-by-default[user preference]. If the
+preference is set so the default behavior is to create `work-in-progress`
+changes, this can be overridden with the `ready` option.
+
 [[message]]
 ==== Message
 
diff --git a/WORKSPACE b/WORKSPACE
index 70200d1..e490c63 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -14,19 +14,9 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    build_file_content = "\n".join([
-        "exports_files([",
-        "    \"0001-Replace-native-http-git-_archive-with-Skylark-rules.patch\",",
-        "    \"0002-Bump-Dagger-to-2.14.1-to-support-Java-9.patch\",",
-        "])",
-    ]),
-    patches = [
-        "//:0001-Replace-native-http-git-_archive-with-Skylark-rules.patch",
-        "//:0002-Bump-Dagger-to-2.14.1-to-support-Java-9.patch",
-    ],
-    sha256 = "a80acb69c63d5f6437b099c111480a4493bad4592015af2127a2f49fb7512d8d",
-    strip_prefix = "rules_closure-0.7.0",
-    url = "https://github.com/bazelbuild/rules_closure/archive/0.7.0.tar.gz",
+    sha256 = "4dd84dd2bdd6c9f56cb5a475d504ea31d199c34309e202e9379501d01c3067e5",
+    strip_prefix = "rules_closure-3103a773820b59b76345f94c231cb213e0d404e2",
+    url = "https://github.com/bazelbuild/rules_closure/archive/3103a773820b59b76345f94c231cb213e0d404e2.tar.gz",
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -888,8 +878,8 @@
 
 maven_jar(
     name = "commons-io",
-    artifact = "commons-io:commons-io:1.4",
-    sha1 = "a8762d07e76cfde2395257a5da47ba7c1dbd3dce",
+    artifact = "commons-io:commons-io:2.2",
+    sha1 = "83b5b8a7ba1c08f9e8c8ff2373724e33d3c1e22a",
 )
 
 maven_jar(
@@ -956,6 +946,10 @@
 
 load("//tools/bzl:js.bzl", "bower_archive", "npm_binary")
 
+# NPM binaries bundled along with their dependencies.
+#
+# For full instructions on adding new binaries to the build, see
+# http://gerrit-review.googlesource.com/Documentation/dev-bazel.html#npm-binary
 npm_binary(
     name = "bower",
 )
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
new file mode 100644
index 0000000..c1e34dd
--- /dev/null
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -0,0 +1,21 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+def acceptance_tests(
+        group,
+        deps = [],
+        labels = [],
+        vm_args = ["-Xmx256m"],
+        **kwargs):
+    junit_tests(
+        name = group,
+        deps = deps + [
+            "//gerrit-acceptance-tests:lib",
+        ],
+        tags = labels + [
+            "acceptance",
+            "slow",
+        ],
+        size = "large",
+        jvm_flags = vm_args,
+        **kwargs
+    )
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
index 1dcb284..fbdf52c 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -151,6 +151,9 @@
   public final native boolean
       publishCommentsOnPush() /*-{ return this.publish_comments_on_push || false }-*/;
 
+  public final native boolean
+      workInProgressByDefault() /*-{ return this.work_in_progress_by_default || false }-*/;
+
   public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/;
 
   public final native void changesPerPage(int n) /*-{ this.changes_per_page = n }-*/;
@@ -230,6 +233,9 @@
   public final native void publishCommentsOnPush(
       boolean p) /*-{ this.publish_comments_on_push = p }-*/;
 
+  public final native void workInProgressByDefault(
+      boolean p) /*-{ this.work_in_progress_by_default = p }-*/;
+
   public final void setMyMenus(List<TopMenuItem> myMenus) {
     initMy();
     for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 0b32cd5..3e21619 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -69,6 +69,8 @@
 
   String publishCommentsOnPush();
 
+  String workInProgressByDefault();
+
   String myMenu();
 
   String myMenuInfo();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 4b01513..c32efed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -39,6 +39,7 @@
 muteCommonPathPrefixes = Mute Common Path Prefixes In File List
 signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
 publishCommentsOnPush = Publish Comments On Push
+workInProgressByDefault = Set all new changes work-in-progress by default
 myMenu = My Menu
 myMenuInfo = \
   Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index f349065..afb8718 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -56,6 +56,7 @@
   private CheckBox muteCommonPathPrefixes;
   private CheckBox signedOffBy;
   private CheckBox publishCommentsOnPush;
+  private CheckBox workInProgressByDefault;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
@@ -163,9 +164,10 @@
     muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
     signedOffBy = new CheckBox(Util.C.signedOffBy());
     publishCommentsOnPush = new CheckBox(Util.C.publishCommentsOnPush());
+    workInProgressByDefault = new CheckBox(Util.C.workInProgressByDefault());
 
     boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
-    final Grid formGrid = new Grid(15 + (flashClippy ? 1 : 0), 2);
+    final Grid formGrid = new Grid(16 + (flashClippy ? 1 : 0), 2);
 
     int row = 0;
 
@@ -229,6 +231,10 @@
     formGrid.setWidget(row, fieldIdx, publishCommentsOnPush);
     row++;
 
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, workInProgressByDefault);
+    row++;
+
     if (flashClippy) {
       formGrid.setText(row, labelIdx, "");
       formGrid.setWidget(row, fieldIdx, useFlashClipboard);
@@ -264,6 +270,7 @@
     e.listenTo(muteCommonPathPrefixes);
     e.listenTo(signedOffBy);
     e.listenTo(publishCommentsOnPush);
+    e.listenTo(workInProgressByDefault);
     e.listenTo(diffView);
     e.listenTo(reviewCategoryStrategy);
     e.listenTo(emailStrategy);
@@ -303,6 +310,7 @@
     muteCommonPathPrefixes.setEnabled(on);
     signedOffBy.setEnabled(on);
     publishCommentsOnPush.setEnabled(on);
+    workInProgressByDefault.setEnabled(on);
     reviewCategoryStrategy.setEnabled(on);
     diffView.setEnabled(on);
     emailStrategy.setEnabled(on);
@@ -329,6 +337,7 @@
     muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
     signedOffBy.setValue(p.signedOffBy());
     publishCommentsOnPush.setValue(p.publishCommentsOnPush());
+    workInProgressByDefault.setValue(p.workInProgressByDefault());
     setListBox(
         reviewCategoryStrategy,
         GeneralPreferencesInfo.ReviewCategoryStrategy.NONE,
@@ -421,6 +430,7 @@
     p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
     p.signedOffBy(signedOffBy.getValue());
     p.publishCommentsOnPush(publishCommentsOnPush.getValue());
+    p.workInProgressByDefault(workInProgressByDefault.getValue());
     p.reviewCategoryStrategy(
         getListBox(
             reviewCategoryStrategy, ReviewCategoryStrategy.NONE, ReviewCategoryStrategy.values()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index c0947a8..9def3b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -79,6 +79,8 @@
 
   String privateByDefault();
 
+  String workInProgressByDefault();
+
   String enableReviewerByEmail();
 
   String matchAuthorToCommitterDate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 8d6878f..1d1bb6d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -31,6 +31,7 @@
 requireChangeID = Require <code>Change-Id</code> in commit message
 rejectImplicitMerges = Reject implicit merges when changes are pushed for review
 privateByDefault = Set all new changes private by default
+workInProgressByDefault = Set all new changes work-in-progress by default
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 64e147d..751e951 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -88,6 +88,7 @@
   private ListBox requireSignedPush;
   private ListBox rejectImplicitMerges;
   private ListBox privateByDefault;
+  private ListBox workInProgressByDefault;
   private ListBox enableReviewerByEmail;
   private ListBox matchAuthorToCommitterDate;
   private NpTextBox maxObjectSizeLimit;
@@ -198,6 +199,7 @@
     requireChangeID.setEnabled(isOwner);
     rejectImplicitMerges.setEnabled(isOwner);
     privateByDefault.setEnabled(isOwner);
+    workInProgressByDefault.setEnabled(isOwner);
     maxObjectSizeLimit.setEnabled(isOwner);
     enableReviewerByEmail.setEnabled(isOwner);
     matchAuthorToCommitterDate.setEnabled(isOwner);
@@ -278,6 +280,10 @@
     saveEnabler.listenTo(privateByDefault);
     grid.addHtml(AdminConstants.I.privateByDefault(), privateByDefault);
 
+    workInProgressByDefault = newInheritedBooleanBox();
+    saveEnabler.listenTo(workInProgressByDefault);
+    grid.addHtml(AdminConstants.I.workInProgressByDefault(), workInProgressByDefault);
+
     enableReviewerByEmail = newInheritedBooleanBox();
     saveEnabler.listenTo(enableReviewerByEmail);
     grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail);
@@ -427,6 +433,7 @@
     }
     setBool(rejectImplicitMerges, result.rejectImplicitMerges());
     setBool(privateByDefault, result.privateByDefault());
+    setBool(workInProgressByDefault, result.workInProgressByDefault());
     setBool(enableReviewerByEmail, result.enableReviewerByEmail());
     setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate());
     setSubmitType(result.defaultSubmitType());
@@ -700,6 +707,7 @@
         rsp,
         getBool(rejectImplicitMerges),
         getBool(privateByDefault),
+        getBool(workInProgressByDefault),
         getBool(enableReviewerByEmail),
         getBool(matchAuthorToCommitterDate),
         maxObjectSizeLimit.getText().trim(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index cf40762..d942c2e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -137,7 +137,10 @@
 
   private static void append(StringBuilder s, JsArrayString lines) {
     for (int i = 0; i < lines.length(); i++) {
-      s.append(lines.get(i)).append('\n');
+      if (s.length() > 0) {
+        s.append('\n');
+      }
+      s.append(lines.get(i));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index f670ac7..4185ef3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -60,6 +60,9 @@
   public final native InheritedBooleanInfo privateByDefault()
       /*-{ return this.private_by_default; }-*/ ;
 
+  public final native InheritedBooleanInfo workInProgressByDefault()
+      /*-{ return this.work_in_progress_by_default; }-*/ ;
+
   public final native InheritedBooleanInfo enableReviewerByEmail()
       /*-{ return this.enable_reviewer_by_email; }-*/ ;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 66afdb2..3be52e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -153,6 +153,7 @@
       InheritableBoolean requireSignedPush,
       InheritableBoolean rejectImplicitMerges,
       InheritableBoolean privateByDefault,
+      InheritableBoolean workInProgressByDefault,
       InheritableBoolean enableReviewerByEmail,
       InheritableBoolean matchAuthorToCommitterDate,
       String maxObjectSizeLimit,
@@ -175,6 +176,7 @@
     }
     in.setRejectImplicitMerges(rejectImplicitMerges);
     in.setPrivateByDefault(privateByDefault);
+    in.setWorkInProgressByDefault(workInProgressByDefault);
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     if (submitType != null) {
       in.setSubmitType(submitType);
@@ -313,6 +315,13 @@
 
     private native void setPrivateByDefault(String v) /*-{ if(v)this.private_by_default=v; }-*/;
 
+    final void setWorkInProgressByDefault(InheritableBoolean v) {
+      setWorkInProgressByDefault(v.name());
+    }
+
+    private native void setWorkInProgressByDefault(
+        String v) /*-{ if(v)this.work_in_progress_by_default=v; }-*/;
+
     final void setEnableReviewerByEmail(InheritableBoolean v) {
       setEnableReviewerByEmailRaw(v.name());
     }
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index abb28ce..960b558 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -68,6 +68,10 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
@@ -108,8 +112,6 @@
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.index.group.GroupIndexer;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.send.EmailHeader;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.MutableNotesMigration;
@@ -274,6 +276,7 @@
 
   @Inject private ChangeIndexCollection changeIndexes;
   @Inject private AccountIndexCollection accountIndexes;
+  @Inject private ProjectIndexCollection projectIndexes;
   @Inject private EventRecorder.Factory eventRecorderFactory;
   @Inject private InProcessProtocol inProcessProtocol;
   @Inject private Provider<AnonymousUser> anonymousUser;
@@ -900,6 +903,41 @@
     };
   }
 
+  protected AutoCloseable disableProjectIndex() {
+    disableProjectIndexWrites();
+    ProjectIndex searchIndex = projectIndexes.getSearchIndex();
+    if (!(searchIndex instanceof DisabledProjectIndex)) {
+      projectIndexes.setSearchIndex(new DisabledProjectIndex(searchIndex), false);
+    }
+
+    return new AutoCloseable() {
+      @Override
+      public void close() {
+        enableProjectIndexWrites();
+        ProjectIndex searchIndex = projectIndexes.getSearchIndex();
+        if (searchIndex instanceof DisabledProjectIndex) {
+          projectIndexes.setSearchIndex(((DisabledProjectIndex) searchIndex).unwrap(), false);
+        }
+      }
+    };
+  }
+
+  protected void disableProjectIndexWrites() {
+    for (ProjectIndex i : projectIndexes.getWriteIndexes()) {
+      if (!(i instanceof DisabledProjectIndex)) {
+        projectIndexes.addWriteIndex(new DisabledProjectIndex(i));
+      }
+    }
+  }
+
+  protected void enableProjectIndexWrites() {
+    for (ProjectIndex i : projectIndexes.getWriteIndexes()) {
+      if (i instanceof DisabledProjectIndex) {
+        projectIndexes.addWriteIndex(((DisabledProjectIndex) i).unwrap());
+      }
+    }
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
@@ -1307,8 +1345,7 @@
 
   protected void assertDiffForNewFile(
       DiffInfo diff, RevCommit commit, String path, String expectedContentSideB) throws Exception {
-    List<String> expectedLines = new ArrayList<>();
-    Collections.addAll(expectedLines, expectedContentSideB.split("\n"));
+    List<String> expectedLines = ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
 
     assertThat(diff.binary).isNull();
     assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 2336f2f..f2d0f70 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -34,10 +34,10 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.mail.EmailHeader.AddressList;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.send.EmailHeader;
-import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 25e1d7c..0214cea 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -19,6 +19,7 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/launcher",
         "//java/com/google/gerrit/lucene",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/pgm",
         "//java/com/google/gerrit/pgm/init",
@@ -98,7 +99,9 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/index",
+        "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lucene",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/pgm/init",
         "//java/com/google/gerrit/reviewdb:server",
diff --git a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
new file mode 100644
index 0000000..2524a76
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.reviewdb.client.Project;
+
+/**
+ * This class wraps an index and assumes the search index can't handle any queries. However, it does
+ * return the current schema as the assumption is that we need a search index for starting Gerrit in
+ * the first place and only later lose the index connection (making it so that we can't send
+ * requests there anymore).
+ */
+public class DisabledProjectIndex implements ProjectIndex {
+  private final ProjectIndex index;
+
+  public DisabledProjectIndex(ProjectIndex index) {
+    this.index = index;
+  }
+
+  public ProjectIndex unwrap() {
+    return index;
+  }
+
+  @Override
+  public Schema<ProjectData> getSchema() {
+    return index.getSchema();
+  }
+
+  @Override
+  public void close() {
+    index.close();
+  }
+
+  @Override
+  public void replace(ProjectData obj) {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+
+  @Override
+  public void delete(Project.NameKey key) {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+
+  @Override
+  public void deleteAll() {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+
+  @Override
+  public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts) {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+
+  @Override
+  public void markReady(boolean ready) {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index f4a8da3..1fed8f8 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 094e8b0..5ce44ff 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -17,8 +17,8 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.net.InetAddresses;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.mail.Address;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
 import java.util.List;
diff --git a/java/com/google/gerrit/common/data/GroupReference.java b/java/com/google/gerrit/common/data/GroupReference.java
index dc22d62..cfaad17 100644
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ b/java/com/google/gerrit/common/data/GroupReference.java
@@ -22,11 +22,6 @@
 
   private static final String PREFIX = "group ";
 
-  /** @return a new reference to the given group description. */
-  public static GroupReference forGroup(AccountGroup group) {
-    return new GroupReference(group.getGroupUUID(), group.getName());
-  }
-
   public static GroupReference forGroup(GroupDescription.Basic group) {
     return new GroupReference(group.getGroupUUID(), group.getName());
   }
@@ -66,6 +61,9 @@
   }
 
   public void setName(String newName) {
+    if (newName == null) {
+      throw new NullPointerException();
+    }
     this.name = newName;
   }
 
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 3755faa..f322c3d 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -21,6 +21,7 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
@@ -79,6 +80,7 @@
   protected static final String MAPPINGS = "mappings";
   protected static final String ORDER = "order";
   protected static final String SEARCH = "_search";
+  protected static final String SETTINGS = "settings";
 
   protected static <T> List<T> decodeProtos(
       JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
@@ -181,7 +183,8 @@
     }
 
     // Recreate the index.
-    response = performRequest("PUT", getMappings(), indexName, Collections.emptyMap());
+    String indexCreationFields = concatJsonString(getSettings(), getMappings());
+    response = performRequest("PUT", indexCreationFields, indexName, Collections.emptyMap());
     statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       String error = String.format("Failed to create index %s: %s", indexName, statusCode);
@@ -193,6 +196,10 @@
 
   protected abstract String getMappings();
 
+  private String getSettings() {
+    return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting()));
+  }
+
   protected abstract String getId(V v);
 
   protected String getMappingsForSingleType(String candidateType, MappingProperties properties) {
@@ -294,6 +301,10 @@
     return performRequest("POST", payload, uri, params);
   }
 
+  private String concatJsonString(String target, String addition) {
+    return target.substring(0, target.length() - 1) + "," + addition.substring(1);
+  }
+
   private Response performRequest(
       String method, Object payload, String uri, Map<String, String> params) throws IOException {
     String payloadStr = payload instanceof String ? (String) payload : payload.toString();
diff --git a/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
index a30e546..f8c4168 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -34,9 +34,9 @@
           || fieldType == FieldType.INTEGER_RANGE
           || fieldType == FieldType.LONG) {
         mapping.addNumber(name);
-      } else if (fieldType == FieldType.PREFIX
-          || fieldType == FieldType.FULL_TEXT
-          || fieldType == FieldType.STORED_ONLY) {
+      } else if (fieldType == FieldType.FULL_TEXT) {
+        mapping.addStringWithAnalyzer(name);
+      } else if (fieldType == FieldType.PREFIX || fieldType == FieldType.STORED_ONLY) {
         mapping.addString(name);
       } else {
         throw new IllegalStateException("Unsupported field type: " + fieldType.getName());
@@ -88,6 +88,13 @@
       return this;
     }
 
+    Builder addStringWithAnalyzer(String name) {
+      FieldProperties key = new FieldProperties(adapter.stringFieldType());
+      key.analyzer = "custom_with_char_filter";
+      fields.put(name, key);
+      return this;
+    }
+
     Builder add(String name, String type) {
       fields.put(name, new FieldProperties(type));
       return this;
@@ -102,6 +109,7 @@
     String type;
     String index;
     String format;
+    String analyzer;
     Map<String, FieldProperties> fields;
 
     FieldProperties(String type) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
new file mode 100644
index 0000000..6fd234d
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+class ElasticSetting {
+  /** The custom char mappings of "." to " " and "_" to " " in the form of UTF-8 */
+  private static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
+      ImmutableMap.of("\\u002E", "\\u0020", "\\u005F", "\\u0020");
+
+  static SettingProperties createSetting() {
+    ElasticSetting.Builder settings = new ElasticSetting.Builder();
+    settings.addCharFilter();
+    settings.addAnalyzer();
+    return settings.build();
+  }
+
+  static class Builder {
+    private final ImmutableMap.Builder<String, FieldProperties> fields =
+        new ImmutableMap.Builder<>();
+
+    SettingProperties build() {
+      SettingProperties properties = new SettingProperties();
+      properties.analysis = fields.build();
+      return properties;
+    }
+
+    void addCharFilter() {
+      FieldProperties charMapping = new FieldProperties("mapping");
+      charMapping.mappings = getCustomCharMappings(CUSTOM_CHAR_MAPPING);
+
+      FieldProperties charFilter = new FieldProperties();
+      charFilter.customMapping = charMapping;
+      fields.put("char_filter", charFilter);
+    }
+
+    void addAnalyzer() {
+      FieldProperties customAnalyzer = new FieldProperties("custom");
+      customAnalyzer.tokenizer = "standard";
+      customAnalyzer.charFilter = new String[] {"custom_mapping"};
+      customAnalyzer.filter = new String[] {"lowercase"};
+
+      FieldProperties analyzer = new FieldProperties();
+      analyzer.customWithCharFilter = customAnalyzer;
+      fields.put("analyzer", analyzer);
+    }
+
+    private static String[] getCustomCharMappings(ImmutableMap<String, String> map) {
+      int mappingIndex = 0;
+      int numOfMappings = map.size();
+      String[] mapping = new String[numOfMappings];
+      for (Map.Entry<String, String> e : map.entrySet()) {
+        mapping[mappingIndex++] = e.getKey() + "=>" + e.getValue();
+      }
+      return mapping;
+    }
+  }
+
+  static class SettingProperties {
+    Map<String, FieldProperties> analysis;
+  }
+
+  static class FieldProperties {
+    String tokenizer;
+    String type;
+    String[] charFilter;
+    String[] filter;
+    String[] mappings;
+    FieldProperties customMapping;
+    FieldProperties customWithCharFilter;
+
+    FieldProperties() {}
+
+    FieldProperties(String type) {
+      this.type = type;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 80115aa..b3dd1f1 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -33,6 +33,7 @@
   public InheritedBooleanInfo requireSignedPush;
   public InheritedBooleanInfo rejectImplicitMerges;
   public InheritedBooleanInfo privateByDefault;
+  public InheritedBooleanInfo workInProgressByDefault;
   public InheritedBooleanInfo enableReviewerByEmail;
   public InheritedBooleanInfo matchAuthorToCommitterDate;
   public InheritedBooleanInfo rejectEmptyCommit;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 37a2e8b..1a6d77b 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -30,6 +30,7 @@
   public InheritableBoolean requireSignedPush;
   public InheritableBoolean rejectImplicitMerges;
   public InheritableBoolean privateByDefault;
+  public InheritableBoolean workInProgressByDefault;
   public InheritableBoolean enableReviewerByEmail;
   public InheritableBoolean matchAuthorToCommitterDate;
   public InheritableBoolean rejectEmptyCommit;
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 9dcba5e..1f16d8d 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -158,6 +158,7 @@
   public EmailFormat emailFormat;
   public DefaultBase defaultBaseForMerges;
   public Boolean publishCommentsOnPush;
+  public Boolean workInProgressByDefault;
 
   public boolean isShowInfoInReviewCategory() {
     return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
@@ -227,6 +228,7 @@
     p.signedOffBy = false;
     p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
     p.publishCommentsOnPush = false;
+    p.workInProgressByDefault = false;
     return p;
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index 6918325..057a1a2 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -47,4 +47,16 @@
     DiffInfo diffInfo = actual();
     return Truth.assertThat(diffInfo.changeType).named("changeType");
   }
+
+  public FileMetaSubject metaA() {
+    isNotNull();
+    DiffInfo diffInfo = actual();
+    return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+  }
+
+  public FileMetaSubject metaB() {
+    isNotNull();
+    DiffInfo diffInfo = actual();
+    return FileMetaSubject.assertThat(diffInfo.metaB).named("metaB");
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
new file mode 100644
index 0000000..e77eef1
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
+
+public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
+
+  public static FileMetaSubject assertThat(FileMeta fileMeta) {
+    return assertAbout(FileMetaSubject::new).that(fileMeta);
+  }
+
+  private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
+    super(failureMetadata, fileMeta);
+  }
+
+  public IntegerSubject totalLineCount() {
+    isNotNull();
+    FileMeta fileMeta = actual();
+    return Truth.assertThat(fileMeta.lines).named("total line count");
+  }
+}
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index bbb5b66..3e71098 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index 96726ad..7315ce1 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/audit",
         "//lib:gson",
         "//lib:guava",
         "//lib:gwtorm",
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index bfb2551..f80e9d5 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/util/http",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/audit",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 292ceff..d557c0e 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -18,6 +18,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
         "//java/com/google/gerrit/server/git/receive",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 9ce7690..8e380f5 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.api.GerritApiModule;
+import com.google.gerrit.server.audit.AuditModule;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -342,6 +343,7 @@
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new LocalMergeSuperSetComputation.Module());
+    modules.add(new AuditModule());
 
     // Plugin module needs to be inserted *before* the index module.
     // There is the concept of LifecycleModule, in Gerrit's own extension
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 0ae9c4c..0bcd8f8 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -127,8 +127,12 @@
           replace(config, toDelete, section);
 
         } else if (AccessSection.isValid(name)) {
-          if (checkIfOwner && !forProject.ref(name).test(RefPermission.WRITE_CONFIG)) {
-            continue;
+          if (checkIfOwner) {
+            try {
+              forProject.ref(name).check(RefPermission.WRITE_CONFIG);
+            } catch (AuthException e) {
+              continue;
+            }
           }
 
           RefPattern.validate(name);
@@ -143,8 +147,15 @@
             config.remove(config.getAccessSection(name));
           }
 
-        } else if (!checkIfOwner || forProject.ref(name).test(RefPermission.WRITE_CONFIG)) {
+        } else if (!checkIfOwner) {
           config.remove(config.getAccessSection(name));
+        } else {
+          try {
+            forProject.ref(name).check(RefPermission.WRITE_CONFIG);
+            config.remove(config.getAccessSection(name));
+          } catch (AuthException e) {
+            // Do nothing.
+          }
         }
       }
 
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 2442b593..6604ca1 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -37,6 +37,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/reviewdb:server",
         "//lib:guava",
         "//lib:gwtjsonrpc",
         "//lib:gwtorm",
diff --git a/java/com/google/gerrit/server/index/RefState.java b/java/com/google/gerrit/index/RefState.java
similarity index 98%
rename from java/com/google/gerrit/server/index/RefState.java
rename to java/com/google/gerrit/index/RefState.java
index 6b893f0..f0e465d 100644
--- a/java/com/google/gerrit/server/index/RefState.java
+++ b/java/com/google/gerrit/index/RefState.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.index;
+package com.google.gerrit.index;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
diff --git a/java/com/google/gerrit/index/project/ProjectData.java b/java/com/google/gerrit/index/project/ProjectData.java
index 9d6b571..7365660 100644
--- a/java/com/google/gerrit/index/project/ProjectData.java
+++ b/java/com/google/gerrit/index/project/ProjectData.java
@@ -14,23 +14,43 @@
 
 package com.google.gerrit.index.project;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.Project;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
 
 public class ProjectData {
   private final Project project;
-  private final ImmutableList<Project.NameKey> ancestors;
+  private final Optional<ProjectData> parent;
 
-  public ProjectData(Project project, Iterable<Project.NameKey> ancestors) {
+  public ProjectData(Project project, Optional<ProjectData> parent) {
     this.project = project;
-    this.ancestors = ImmutableList.copyOf(ancestors);
+    this.parent = parent;
   }
 
   public Project getProject() {
     return project;
   }
 
-  public ImmutableList<Project.NameKey> getAncestors() {
-    return ancestors;
+  public Optional<ProjectData> getParent() {
+    return parent;
+  }
+
+  /** Returns all {@link ProjectData} in the hierarchy starting with the current one. */
+  public ImmutableList<ProjectData> tree() {
+    List<ProjectData> parents = new ArrayList<>();
+    Optional<ProjectData> curr = Optional.of(this);
+    while (curr.isPresent()) {
+      parents.add(curr.get());
+      curr = curr.get().parent;
+    }
+    return ImmutableList.copyOf(parents);
+  }
+
+  public ImmutableList<String> getParentNames() {
+    return tree().stream().skip(1).map(p -> p.getProject().getName()).collect(toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 1c2f629b..5e484b2 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -14,23 +14,30 @@
 
 package com.google.gerrit.index.project;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.prefix;
+import static com.google.gerrit.index.FieldDef.storedOnly;
 
-import com.google.common.collect.Iterables;
 import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 /** Index schema for projects. */
 public class ProjectField {
+  private static byte[] toRefState(Project project) {
+    return RefState.create(RefNames.REFS_CONFIG, project.getConfigRefState())
+        .toByteArray(project.getNameKey());
+  }
 
   public static final FieldDef<ProjectData, String> NAME =
       exact("name").stored().build(p -> p.getProject().getName());
 
   public static final FieldDef<ProjectData, String> DESCRIPTION =
-      fullText("description").build(p -> p.getProject().getDescription());
+      fullText("description").stored().build(p -> p.getProject().getDescription());
 
   public static final FieldDef<ProjectData, String> PARENT_NAME =
       exact("parent_name").build(p -> p.getProject().getParentName());
@@ -38,7 +45,26 @@
   public static final FieldDef<ProjectData, Iterable<String>> NAME_PART =
       prefix("name_part").buildRepeatable(p -> SchemaUtil.getNameParts(p.getProject().getName()));
 
+  public static final FieldDef<ProjectData, String> STATE =
+      exact("state").stored().build(p -> p.getProject().getState().name());
+
   public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
-      exact("ancestor_name")
-          .buildRepeatable(p -> Iterables.transform(p.getAncestors(), Project.NameKey::get));
+      exact("ancestor_name").buildRepeatable(p -> p.getParentNames());
+
+  /**
+   * All values of all refs that were used in the course of indexing this document. This covers
+   * {@code refs/meta/config} of the current project and all of its parents.
+   *
+   * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}.
+   */
+  public static final FieldDef<ProjectData, Iterable<byte[]>> REF_STATE =
+      storedOnly("ref_state")
+          .buildRepeatable(
+              projectData ->
+                  projectData
+                      .tree()
+                      .stream()
+                      .filter(p -> p.getProject().getConfigRefState() != null)
+                      .map(p -> toRefState(p.getProject()))
+                      .collect(toImmutableList()));
 }
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index cbea4fe..07b5adb 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -21,6 +21,7 @@
 
 public class ProjectSchemaDefinitions extends SchemaDefinitions<ProjectData> {
 
+  @Deprecated
   static final Schema<ProjectData> V1 =
       schema(
           ProjectField.NAME,
@@ -29,6 +30,8 @@
           ProjectField.NAME_PART,
           ProjectField.ANCESTOR_NAME);
 
+  static final Schema<ProjectData> V2 = schema(V1, ProjectField.STATE, ProjectField.REF_STATE);
+
   public static final ProjectSchemaDefinitions INSTANCE = new ProjectSchemaDefinitions();
 
   private ProjectSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/mail/Address.java b/java/com/google/gerrit/mail/Address.java
similarity index 95%
rename from java/com/google/gerrit/server/mail/Address.java
rename to java/com/google/gerrit/mail/Address.java
index e91f3f3..24ab353 100644
--- a/java/com/google/gerrit/server/mail/Address.java
+++ b/java/com/google/gerrit/mail/Address.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail;
+package com.google.gerrit.mail;
 
-import com.google.gerrit.server.mail.send.EmailHeader;
+import com.google.gerrit.common.Nullable;
 
 public class Address {
   public static Address parse(String in) {
@@ -50,8 +50,8 @@
     }
   }
 
-  final String name;
-  final String email;
+  @Nullable private final String name;
+  private final String email;
 
   public Address(String email) {
     this(null, email);
@@ -62,6 +62,7 @@
     this.email = email;
   }
 
+  @Nullable
   public String getName() {
     return name;
   }
diff --git a/java/com/google/gerrit/mail/BUILD b/java/com/google/gerrit/mail/BUILD
new file mode 100644
index 0000000..90bb82c
--- /dev/null
+++ b/java/com/google/gerrit/mail/BUILD
@@ -0,0 +1,18 @@
+java_library(
+    name = "mail",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib:guava",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
+        "//lib/jsoup",
+        "//lib/log:jsonevent-layout",
+        "//lib/log:log4j",
+        "//lib/mime4j:core",
+        "//lib/mime4j:dom",
+    ],
+)
diff --git a/java/com/google/gerrit/server/mail/send/EmailHeader.java b/java/com/google/gerrit/mail/EmailHeader.java
similarity index 97%
rename from java/com/google/gerrit/server/mail/send/EmailHeader.java
rename to java/com/google/gerrit/mail/EmailHeader.java
index 29354f2..69d5fcd 100644
--- a/java/com/google/gerrit/server/mail/send/EmailHeader.java
+++ b/java/com/google/gerrit/mail/EmailHeader.java
@@ -12,12 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.send;
+package com.google.gerrit.mail;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.MoreObjects;
-import com.google.gerrit.server.mail.Address;
 import java.io.IOException;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
@@ -183,7 +182,7 @@
       list.add(addr);
     }
 
-    void remove(java.lang.String email) {
+    public void remove(java.lang.String email) {
       list.removeIf(address -> address.getEmail().equals(email));
     }
 
diff --git a/java/com/google/gerrit/server/mail/receive/HtmlParser.java b/java/com/google/gerrit/mail/HtmlParser.java
similarity index 98%
rename from java/com/google/gerrit/server/mail/receive/HtmlParser.java
rename to java/com/google/gerrit/mail/HtmlParser.java
index d68f076..9821599 100644
--- a/java/com/google/gerrit/server/mail/receive/HtmlParser.java
+++ b/java/com/google/gerrit/mail/HtmlParser.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
diff --git a/java/com/google/gerrit/server/mail/receive/MailComment.java b/java/com/google/gerrit/mail/MailComment.java
similarity index 84%
rename from java/com/google/gerrit/server/mail/receive/MailComment.java
rename to java/com/google/gerrit/mail/MailComment.java
index 8571e12..fd8198c 100644
--- a/java/com/google/gerrit/server/mail/receive/MailComment.java
+++ b/java/com/google/gerrit/mail/MailComment.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.gerrit.reviewdb.client.Comment;
 import java.util.Objects;
 
 /** A comment parsed from inbound email */
 public class MailComment {
-  enum CommentType {
+  public enum CommentType {
     CHANGE_MESSAGE,
     FILE_COMMENT,
     INLINE_COMMENT
@@ -42,6 +42,22 @@
     this.isLink = isLink;
   }
 
+  public CommentType getType() {
+    return type;
+  }
+
+  public Comment getInReplyTo() {
+    return inReplyTo;
+  }
+
+  public String getFileName() {
+    return fileName;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
   /**
    * Checks if the provided comment concerns the same exact spot in the change. This is basically an
    * equals method except that the message is not checked.
diff --git a/java/com/google/gerrit/server/mail/MailHeader.java b/java/com/google/gerrit/mail/MailHeader.java
similarity index 97%
rename from java/com/google/gerrit/server/mail/MailHeader.java
rename to java/com/google/gerrit/mail/MailHeader.java
index cf145e5..2f31a9c 100644
--- a/java/com/google/gerrit/server/mail/MailHeader.java
+++ b/java/com/google/gerrit/mail/MailHeader.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail;
+package com.google.gerrit.mail;
 
 /** Variables used by emails to hold data */
 public enum MailHeader {
diff --git a/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java b/java/com/google/gerrit/mail/MailHeaderParser.java
similarity index 92%
rename from java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
rename to java/com/google/gerrit/mail/MailHeaderParser.java
index d176095..8eb4d97 100644
--- a/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
+++ b/java/com/google/gerrit/mail/MailHeaderParser.java
@@ -12,14 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.server.mail.MailHeader;
-import com.google.gerrit.server.mail.MailUtil;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.format.DateTimeParseException;
@@ -44,7 +42,8 @@
       } else if (header.startsWith(MailHeader.COMMENT_DATE.fieldWithDelimiter())) {
         String ts = header.substring(MailHeader.COMMENT_DATE.fieldWithDelimiter().length()).trim();
         try {
-          metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+          metadata.timestamp =
+              Timestamp.from(MailProcessingUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
           logger.atSevere().withCause(e).log(
               "Mail: Error while parsing timestamp from header of message %s", m.id());
@@ -91,7 +90,8 @@
       } else if (metadata.timestamp == null && line.contains(MailHeader.COMMENT_DATE.getName())) {
         String ts = extractFooter(MailHeader.COMMENT_DATE.withDelimiter(), line);
         try {
-          metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+          metadata.timestamp =
+              Timestamp.from(MailProcessingUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
           logger.atSevere().withCause(e).log(
               "Mail: Error while parsing timestamp from footer of message %s", m.id());
diff --git a/java/com/google/gerrit/server/mail/receive/MailMessage.java b/java/com/google/gerrit/mail/MailMessage.java
similarity index 96%
rename from java/com/google/gerrit/server/mail/receive/MailMessage.java
rename to java/com/google/gerrit/mail/MailMessage.java
index 0d20464..bb83dfd 100644
--- a/java/com/google/gerrit/server/mail/receive/MailMessage.java
+++ b/java/com/google/gerrit/mail/MailMessage.java
@@ -12,12 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.mail.Address;
 import java.time.Instant;
 
 /**
diff --git a/java/com/google/gerrit/server/mail/receive/MailMetadata.java b/java/com/google/gerrit/mail/MailMetadata.java
similarity index 96%
rename from java/com/google/gerrit/server/mail/receive/MailMetadata.java
rename to java/com/google/gerrit/mail/MailMetadata.java
index 04c2add..a311461 100644
--- a/java/com/google/gerrit/server/mail/receive/MailMetadata.java
+++ b/java/com/google/gerrit/mail/MailMetadata.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.common.base.MoreObjects;
 import java.sql.Timestamp;
diff --git a/java/com/google/gerrit/server/mail/receive/MailParsingException.java b/java/com/google/gerrit/mail/MailParsingException.java
similarity index 94%
rename from java/com/google/gerrit/server/mail/receive/MailParsingException.java
rename to java/com/google/gerrit/mail/MailParsingException.java
index b91bb18..7e85a27 100644
--- a/java/com/google/gerrit/server/mail/receive/MailParsingException.java
+++ b/java/com/google/gerrit/mail/MailParsingException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 /** An {@link Exception} indicating that an email could not be parsed. */
 public class MailParsingException extends Exception {
diff --git a/java/com/google/gerrit/mail/MailProcessingUtil.java b/java/com/google/gerrit/mail/MailProcessingUtil.java
new file mode 100644
index 0000000..b63189a
--- /dev/null
+++ b/java/com/google/gerrit/mail/MailProcessingUtil.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.mail;
+
+import java.time.format.DateTimeFormatter;
+
+public class MailProcessingUtil {
+
+  public static DateTimeFormatter rfcDateformatter =
+      DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ");
+}
diff --git a/java/com/google/gerrit/server/mail/receive/ParserUtil.java b/java/com/google/gerrit/mail/ParserUtil.java
similarity index 98%
rename from java/com/google/gerrit/server/mail/receive/ParserUtil.java
rename to java/com/google/gerrit/mail/ParserUtil.java
index e770a3e..6a27ac4 100644
--- a/java/com/google/gerrit/server/mail/receive/ParserUtil.java
+++ b/java/com/google/gerrit/mail/ParserUtil.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
diff --git a/java/com/google/gerrit/server/mail/receive/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
similarity index 98%
rename from java/com/google/gerrit/server/mail/receive/RawMailParser.java
rename to java/com/google/gerrit/mail/RawMailParser.java
index 57fe21f..00754d3 100644
--- a/java/com/google/gerrit/server/mail/receive/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
@@ -21,7 +21,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.io.CharStreams;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.server.mail.Address;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
diff --git a/java/com/google/gerrit/server/mail/receive/TextParser.java b/java/com/google/gerrit/mail/TextParser.java
similarity index 98%
rename from java/com/google/gerrit/server/mail/receive/TextParser.java
rename to java/com/google/gerrit/mail/TextParser.java
index b99c608..1a63599 100644
--- a/java/com/google/gerrit/server/mail/receive/TextParser.java
+++ b/java/com/google/gerrit/mail/TextParser.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 95570ec..64dd1d8 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -36,6 +36,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
         "//java/com/google/gerrit/server/git/receive",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index c38e7f5..517787c 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -56,6 +56,7 @@
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.api.GerritApiModule;
 import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.audit.AuditModule;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -422,6 +423,7 @@
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new GerritApiModule());
     modules.add(new PluginApiModule());
+    modules.add(new AuditModule());
 
     modules.add(new SearchingChangeCacheImpl.Module(slave));
     modules.add(new InternalAccountDirectory.Module());
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
index 765e38c..a70d254 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -40,7 +40,8 @@
   PRIVATE_BY_DEFAULT("change", "privateByDefault"),
   ENABLE_REVIEWER_BY_EMAIL("reviewer", "enableByEmail"),
   MATCH_AUTHOR_TO_COMMITTER_DATE("submit", "matchAuthorToCommitterDate"),
-  REJECT_EMPTY_COMMIT("submit", "rejectEmptyCommit");
+  REJECT_EMPTY_COMMIT("submit", "rejectEmptyCommit"),
+  WORK_IN_PROGRESS_BY_DEFAULT("change", "workInProgressByDefault");
 
   // Git config
   private final String section;
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index 921667e..996f1ec 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -99,6 +99,8 @@
 
   protected String themeName;
 
+  protected String configRefState;
+
   protected Project() {}
 
   public Project(Project.NameKey nameKey) {
@@ -239,4 +241,14 @@
   public void setParentName(NameKey n) {
     parent = n;
   }
+
+  /** Returns the {@code ObjectId} as 40 digit hex of {@code refs/meta/config}'s HEAD. */
+  public String getConfigRefState() {
+    return configRefState;
+  }
+
+  /** Sets the {@code ObjectId} as 40 digit hex of {@code refs/meta/config}'s HEAD. */
+  public void setConfigRefState(String state) {
+    configRefState = state;
+  }
 }
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 0d12eca..3625de6 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -263,12 +263,17 @@
 
   private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
     try {
-      return projectCache.checkedGet(notes.getProjectName()).statePermitsRead()
-          && permissionBackend
-              .absentUser(accountId)
-              .change(notes)
-              .database(db)
-              .test(ChangePermission.READ);
+      if (!projectCache.checkedGet(notes.getProjectName()).statePermitsRead()) {
+        return false;
+      }
+      permissionBackend
+          .absentUser(accountId)
+          .change(notes)
+          .database(db)
+          .check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
     } catch (IOException | PermissionBackendException e) {
       logger.atWarning().withCause(e).log(
           "Failed to check if account %d can see change %d",
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 280a467..e99c686 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -36,10 +36,12 @@
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/util/git",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/ssl",
         "//java/org/apache/commons/net",
diff --git a/java/com/google/gerrit/server/ReviewerByEmailSet.java b/java/com/google/gerrit/server/ReviewerByEmailSet.java
index c16c9c8..caae45e 100644
--- a/java/com/google/gerrit/server/ReviewerByEmailSet.java
+++ b/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Table;
-import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import java.sql.Timestamp;
 
diff --git a/java/com/google/gerrit/server/account/AccountDirectory.java b/java/com/google/gerrit/server/account/AccountDirectory.java
index eeca29c..ee9265f 100644
--- a/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.util.Set;
 
 /**
@@ -47,6 +48,6 @@
     STATUS
   }
 
-  public abstract void fillAccountInfo(
-      Iterable<? extends AccountInfo> in, Set<FillOptions> options);
+  public abstract void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options)
+      throws PermissionBackendException;
 }
diff --git a/java/com/google/gerrit/server/account/AccountLoader.java b/java/com/google/gerrit/server/account/AccountLoader.java
index ddad864..4398d9e 100644
--- a/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/java/com/google/gerrit/server/account/AccountLoader.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.util.ArrayList;
@@ -83,18 +84,18 @@
     provided.add(info);
   }
 
-  public void fill() {
+  public void fill() throws PermissionBackendException {
     directory.fillAccountInfo(Iterables.concat(created.values(), provided), options);
   }
 
-  public void fill(Collection<? extends AccountInfo> infos) {
+  public void fill(Collection<? extends AccountInfo> infos) throws PermissionBackendException {
     for (AccountInfo info : infos) {
       put(info);
     }
     fill();
   }
 
-  public AccountInfo fillOne(Account.Id id) {
+  public AccountInfo fillOne(Account.Id id) throws PermissionBackendException {
     AccountInfo info = get(id);
     fill();
     return info;
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index cf21c70..ce97ff9 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -18,16 +18,23 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AvatarInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -35,6 +42,7 @@
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 @Singleton
@@ -51,22 +59,45 @@
   private final AccountCache accountCache;
   private final DynamicItem<AvatarProvider> avatar;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   InternalAccountDirectory(
       AccountCache accountCache,
       DynamicItem<AvatarProvider> avatar,
-      IdentifiedUser.GenericFactory userFactory) {
+      IdentifiedUser.GenericFactory userFactory,
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend) {
     this.accountCache = accountCache;
     this.avatar = avatar;
     this.userFactory = userFactory;
+    this.self = self;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  public void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options) {
+  public void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options)
+      throws PermissionBackendException {
     if (options.equals(ID_ONLY)) {
       return;
     }
+
+    boolean canModifyAccount = false;
+    Account.Id currentUserId = null;
+    if (self.get().isIdentifiedUser()) {
+      currentUserId = self.get().getAccountId();
+
+      try {
+        permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+        canModifyAccount = true;
+      } catch (AuthException e) {
+        canModifyAccount = false;
+      }
+    }
+
+    Set<FillOptions> fillOptionsWithoutSecondaryEmails =
+        Sets.difference(options, EnumSet.of(FillOptions.SECONDARY_EMAILS));
     Set<Account.Id> ids =
         Streams.stream(in).map(a -> new Account.Id(a._accountId)).collect(toSet());
     Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
@@ -74,7 +105,15 @@
       Account.Id id = new Account.Id(info._accountId);
       AccountState state = accountStates.get(id);
       if (state != null) {
-        fill(info, accountStates.get(id), options);
+        if (!options.contains(FillOptions.SECONDARY_EMAILS)
+            || Objects.equals(currentUserId, state.getAccount().getId())
+            || canModifyAccount) {
+          fill(info, accountStates.get(id), options);
+        } else {
+          // user is not allowed to see secondary emails
+          fill(info, accountStates.get(id), fillOptionsWithoutSecondaryEmails);
+        }
+
       } else {
         info._accountId = options.contains(FillOptions.ID) ? id.get() : null;
       }
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index a1d0c4f..9ef5753 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -79,7 +79,6 @@
 import com.google.gerrit.server.restapi.account.StarredChanges;
 import com.google.gerrit.server.restapi.account.Stars;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
@@ -393,7 +392,7 @@
   public List<GroupInfo> getGroups() throws RestApiException {
     try {
       return getGroups.apply(account);
-    } catch (OrmException e) {
+    } catch (Exception e) {
       throw asRestApiException("Cannot get groups", e);
     }
   }
@@ -518,7 +517,11 @@
 
   @Override
   public List<AgreementInfo> listAgreements() throws RestApiException {
-    return getAgreements.apply(account);
+    try {
+      return getAgreements.apply(account);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get agreements", e);
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/audit/AuditModule.java b/java/com/google/gerrit/server/audit/AuditModule.java
index 0052aaa..df037b6 100644
--- a/java/com/google/gerrit/server/audit/AuditModule.java
+++ b/java/com/google/gerrit/server/audit/AuditModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.audit.group.GroupAuditListener;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.inject.AbstractModule;
 
 public class AuditModule extends AbstractModule {
@@ -24,6 +25,6 @@
   protected void configure() {
     DynamicSet.setOf(binder(), AuditListener.class);
     DynamicSet.setOf(binder(), GroupAuditListener.class);
-    bind(AuditService.class);
+    bind(GroupAuditService.class).to(AuditService.class);
   }
 }
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index 9528670..2055e33 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -22,12 +22,13 @@
 import com.google.gerrit.server.audit.group.GroupAuditListener;
 import com.google.gerrit.server.audit.group.GroupMemberAuditEvent;
 import com.google.gerrit.server.audit.group.GroupSubgroupAuditEvent;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
 
 @Singleton
-public class AuditService {
+public class AuditService implements GroupAuditService {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<AuditListener> auditListeners;
@@ -47,6 +48,7 @@
     }
   }
 
+  @Override
   public void dispatchAddMembers(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
@@ -63,6 +65,7 @@
     }
   }
 
+  @Override
   public void dispatchDeleteMembers(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
@@ -79,6 +82,7 @@
     }
   }
 
+  @Override
   public void dispatchAddSubgroups(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
@@ -95,6 +99,7 @@
     }
   }
 
+  @Override
   public void dispatchDeleteSubgroups(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
new file mode 100644
index 0000000..5efdc5a
--- /dev/null
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -0,0 +1,73 @@
+java_library(
+    name = "audit",
+    srcs = glob(
+        ["**/*.java"],
+    ),
+    resource_strip_prefix = "resources",
+    resources = ["//resources/com/google/gerrit/server"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/index",
+        "//java/com/google/gerrit/index:query_exception",
+        "//java/com/google/gerrit/index/project",
+        "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/prettify:server",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/util/cli",
+        "//java/com/google/gerrit/util/ssl",
+        "//java/org/apache/commons/net",
+        "//java/org/eclipse/jgit:server",
+        "//lib:args4j",
+        "//lib:automaton",
+        "//lib:blame-cache",
+        "//lib:grappa",
+        "//lib:gson",
+        "//lib:guava",
+        "//lib:guava-retrying",
+        "//lib:gwtjsonrpc",
+        "//lib:gwtorm",
+        "//lib:jsch",
+        "//lib:juniversalchardet",
+        "//lib:mime-util",
+        "//lib:pegdown",
+        "//lib:protobuf",
+        "//lib:servlet-api-3_1",
+        "//lib:soy",
+        "//lib:tukaani-xz",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/bouncycastle:bcpkix-neverlink",
+        "//lib/bouncycastle:bcprov-neverlink",
+        "//lib/commons:codec",
+        "//lib/commons:compress",
+        "//lib/commons:dbcp",
+        "//lib/commons:lang",
+        "//lib/commons:net",
+        "//lib/commons:validator",
+        "//lib/flogger:api",
+        "//lib/guice",
+        "//lib/guice:guice-assistedinject",
+        "//lib/guice:guice-servlet",
+        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/jsoup",
+        "//lib/log:jsonevent-layout",
+        "//lib/log:log4j",
+        "//lib/lucene:lucene-analyzers-common",
+        "//lib/lucene:lucene-core-and-backward-codecs",
+        "//lib/lucene:lucene-queryparser",
+        "//lib/mime4j:core",
+        "//lib/mime4j:dom",
+        "//lib/ow2:ow2-asm",
+        "//lib/ow2:ow2-asm-tree",
+        "//lib/ow2:ow2-asm-util",
+        "//lib/prolog:runtime",
+        "//proto:cache_java_proto",
+    ],
+)
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 8ca364b..e8c55e8 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -459,18 +460,24 @@
         .stream()
         .filter(
             accountId -> {
+              if (!projectState.statePermitsRead()) {
+                return false;
+              }
+
               try {
-                return permissionBackend
-                        .absentUser(accountId)
-                        .change(notes)
-                        .database(db)
-                        .test(ChangePermission.READ)
-                    && projectState.statePermitsRead();
+                permissionBackend
+                    .absentUser(accountId)
+                    .change(notes)
+                    .database(db)
+                    .check(ChangePermission.READ);
+                return true;
               } catch (PermissionBackendException e) {
                 logger.atWarning().withCause(e).log(
                     "Failed to check if account %d can see change %d",
                     accountId.get(), notes.getChangeId().get());
                 return false;
+              } catch (AuthException e) {
+                return false;
               }
             })
         .collect(toSet());
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index a5bcf48..04b649b 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -91,6 +91,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
@@ -123,7 +124,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -415,7 +415,8 @@
     return format(cd, Optional.of(rsrc.getPatchSet().getId()), true);
   }
 
-  public List<List<ChangeInfo>> formatQueryResults(List<QueryResult<ChangeData>> in) {
+  public List<List<ChangeInfo>> formatQueryResults(List<QueryResult<ChangeData>> in)
+      throws PermissionBackendException {
     try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) {
       accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
       List<List<ChangeInfo>> res = new ArrayList<>(in.size());
@@ -433,7 +434,8 @@
     }
   }
 
-  public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in) throws OrmException {
+  public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in)
+      throws OrmException, PermissionBackendException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     ensureLoaded(in);
     List<ChangeInfo> out = new ArrayList<>(in.size());
diff --git a/java/com/google/gerrit/server/change/ReviewerResource.java b/java/com/google/gerrit/server/change/ReviewerResource.java
index 778897e..52f3585 100644
--- a/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -19,10 +19,10 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.Address;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 57255a3..b6a257b 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -91,7 +91,6 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
-import com.google.gerrit.server.audit.AuditModule;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
 import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
@@ -301,7 +300,6 @@
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     bind(AccountControl.Factory.class);
 
-    install(new AuditModule());
     bind(UiActions.class);
 
     bind(GitReferenceUpdated.class);
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 7d35070..1aff0fa 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -171,21 +171,31 @@
       return false;
     }
     ReviewDb db = dbProvider.get();
-    return permissionBackend
-        .user(user)
-        .change(notesFactory.createChecked(db, change))
-        .database(db)
-        .test(ChangePermission.READ);
+    try {
+      permissionBackend
+          .user(user)
+          .change(notesFactory.createChecked(db, change))
+          .database(db)
+          .check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
   }
 
   protected boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user)
       throws PermissionBackendException {
     ProjectState pe = projectCache.get(branchName.getParentKey());
-    if (pe == null) {
+    if (pe == null || !pe.statePermitsRead()) {
       return false;
     }
-    return pe.statePermitsRead()
-        && permissionBackend.user(user).ref(branchName).test(RefPermission.READ);
+
+    try {
+      permissionBackend.user(user).ref(branchName).check(RefPermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
   }
 
   protected boolean isVisibleTo(Event event, CurrentUser user)
diff --git a/java/com/google/gerrit/server/git/NotifyConfig.java b/java/com/google/gerrit/server/git/NotifyConfig.java
index d2d3a39..d39cf12 100644
--- a/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -16,8 +16,8 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.mail.Address;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.Set;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index d5b1966..516dcd4 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2204,6 +2204,7 @@
     }
 
     private void setChangeId(int id) {
+      possiblyOverrideWorkInProgress();
 
       changeId = new Change.Id(id);
       ins =
@@ -2224,6 +2225,16 @@
       }
     }
 
+    private void possiblyOverrideWorkInProgress() {
+      // When wip or ready explicitly provided, leave it as is.
+      if (magicBranch.workInProgress || magicBranch.ready) {
+        return;
+      }
+      magicBranch.workInProgress =
+          projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+              || firstNonNull(user.state().getGeneralPreferences().workInProgressByDefault, false);
+    }
+
     private void addOps(BatchUpdate bu) throws RestApiException {
       checkState(changeId != null, "must call setChangeId before addOps");
       try {
@@ -2528,11 +2539,23 @@
       if (magicBranch != null
           && (magicBranch.workInProgress || magicBranch.ready)
           && magicBranch.workInProgress != change.isWorkInProgress()
-          && (!user.getAccountId().equals(change.getOwner())
-              && !permissions.test(ProjectPermission.WRITE_CONFIG)
-              && !permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER))) {
-        reject(inputCommand, ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
-        return false;
+          && !user.getAccountId().equals(change.getOwner())) {
+        boolean hasWriteConfigPermission = false;
+        try {
+          permissions.check(ProjectPermission.WRITE_CONFIG);
+          hasWriteConfigPermission = true;
+        } catch (AuthException e) {
+          // Do nothing.
+        }
+
+        if (!hasWriteConfigPermission) {
+          try {
+            permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+          } catch (AuthException e1) {
+            reject(inputCommand, ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
+            return false;
+          }
+        }
       }
 
       if (magicBranch != null && (magicBranch.edit || magicBranch.draft)) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 57786a6..e4c7b35 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -335,27 +335,26 @@
       StringBuilder sb = new StringBuilder();
       sb.append("ERROR: ").append(errMsg);
 
+      boolean hinted = false;
       if (c.getFullMessage().contains(CHANGE_ID_PREFIX)) {
         String lastLine = Iterables.getLast(Splitter.on('\n').split(c.getFullMessage()), "");
         if (!lastLine.contains(CHANGE_ID_PREFIX)) {
-          sb.append('\n');
-          sb.append('\n');
-          sb.append("Hint: A potential ");
-          sb.append(FooterConstants.CHANGE_ID.getName());
-          sb.append(" was found, but it was not in the ");
-          sb.append("footer (last paragraph) of the commit message.");
+          hinted = true;
+          sb.append("\n\n")
+              .append("Hint: run\n")
+              .append("  git commit --amend\n")
+              .append("and move 'Change-Id: Ixxx..' to the bottom on a separate line\n");
         }
       }
-      sb.append('\n');
-      sb.append('\n');
-      sb.append("Hint: To automatically insert ");
-      sb.append(FooterConstants.CHANGE_ID.getName());
-      sb.append(", install the hook:\n");
-      sb.append(getCommitMessageHookInstallationHint());
-      sb.append('\n');
-      sb.append("And then amend the commit:\n");
-      sb.append("  git commit --amend\n");
 
+      // Print only one hint to avoid overwhelming the user.
+      if (!hinted) {
+        sb.append("Hint: to automatically insert a Change-Id, install the hook:\n")
+            .append(getCommitMessageHookInstallationHint())
+            .append("\n")
+            .append("and then amend the commit:\n")
+            .append("  git commit --amend\n");
+      }
       return new CommitValidationMessage(sb.toString(), false);
     }
 
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
new file mode 100644
index 0000000..c543a6e
--- /dev/null
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
+
+public interface GroupAuditService {
+
+  void dispatchAddMembers(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<Id> addedMembers,
+      Timestamp addedOn);
+
+  void dispatchDeleteMembers(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<Account.Id> deletedMembers,
+      Timestamp deletedOn);
+
+  void dispatchAddSubgroups(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<AccountGroup.UUID> addedSubgroups,
+      Timestamp addedOn);
+
+  void dispatchDeleteSubgroups(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<AccountGroup.UUID> deletedSubgroups,
+      Timestamp deletedOn);
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 38db2e6..cd38c6a 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -31,13 +31,13 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupIncludeCache;
-import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.update.RefUpdateUtil;
@@ -89,7 +89,7 @@
   private final GroupCache groupCache;
   private final GroupIncludeCache groupIncludeCache;
   private final Provider<GroupIndexer> indexer;
-  private final AuditService auditService;
+  private final GroupAuditService groupAuditService;
   private final RenameGroupOp.Factory renameGroupOpFactory;
   @Nullable private final IdentifiedUser currentUser;
   private final AuditLogFormatter auditLogFormatter;
@@ -106,7 +106,7 @@
       GroupCache groupCache,
       GroupIncludeCache groupIncludeCache,
       Provider<GroupIndexer> indexer,
-      AuditService auditService,
+      GroupAuditService auditService,
       AccountCache accountCache,
       RenameGroupOp.Factory renameGroupOpFactory,
       @GerritServerId String serverId,
@@ -120,7 +120,7 @@
     this.groupCache = groupCache;
     this.groupIncludeCache = groupIncludeCache;
     this.indexer = indexer;
-    this.auditService = auditService;
+    this.groupAuditService = auditService;
     this.renameGroupOpFactory = renameGroupOpFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.retryHelper = retryHelper;
@@ -384,14 +384,14 @@
     }
 
     if (!createdGroup.getMembers().isEmpty()) {
-      auditService.dispatchAddMembers(
+      groupAuditService.dispatchAddMembers(
           currentUser.getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getMembers(),
           createdGroup.getCreatedOn());
     }
     if (!createdGroup.getSubgroups().isEmpty()) {
-      auditService.dispatchAddSubgroups(
+      groupAuditService.dispatchAddSubgroups(
           currentUser.getAccountId(),
           createdGroup.getGroupUUID(),
           createdGroup.getSubgroups(),
@@ -405,19 +405,19 @@
     }
 
     if (!result.getAddedMembers().isEmpty()) {
-      auditService.dispatchAddMembers(
+      groupAuditService.dispatchAddMembers(
           currentUser.getAccountId(), result.getGroupUuid(), result.getAddedMembers(), updatedOn);
     }
     if (!result.getDeletedMembers().isEmpty()) {
-      auditService.dispatchDeleteMembers(
+      groupAuditService.dispatchDeleteMembers(
           currentUser.getAccountId(), result.getGroupUuid(), result.getDeletedMembers(), updatedOn);
     }
     if (!result.getAddedSubgroups().isEmpty()) {
-      auditService.dispatchAddSubgroups(
+      groupAuditService.dispatchAddSubgroups(
           currentUser.getAccountId(), result.getGroupUuid(), result.getAddedSubgroups(), updatedOn);
     }
     if (!result.getDeletedSubgroups().isEmpty()) {
-      auditService.dispatchDeleteSubgroups(
+      groupAuditService.dispatchDeleteSubgroups(
           currentUser.getAccountId(),
           result.getGroupUuid(),
           result.getDeletedSubgroups(),
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index 31e2ada..111991c 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -27,11 +27,11 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.index.RefState;
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.Collections;
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
index 6403d3d..0c3e329 100644
--- a/java/com/google/gerrit/server/index/account/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexUtils;
-import com.google.gerrit.server.index.RefState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 405e6fc..5d12e79 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -43,7 +43,9 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -56,9 +58,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index 642385c..0508b60 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -31,12 +31,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UsedAt;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.query.change.ChangeData;
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
new file mode 100644
index 0000000..5603f08
--- /dev/null
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.project;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.RefState;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectField;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.project.ProjectCache;
+import java.io.IOException;
+import java.util.Optional;
+import javax.inject.Inject;
+
+public class StalenessChecker {
+  private static final ImmutableSet<String> FIELDS =
+      ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+
+  private final ProjectCache projectCache;
+  private final ProjectIndexCollection indexes;
+  private final IndexConfig indexConfig;
+
+  @Inject
+  StalenessChecker(
+      ProjectCache projectCache, ProjectIndexCollection indexes, IndexConfig indexConfig) {
+    this.projectCache = projectCache;
+    this.indexes = indexes;
+    this.indexConfig = indexConfig;
+  }
+
+  public boolean isStale(Project.NameKey project) throws IOException {
+    ProjectData projectData = projectCache.get(project).toProjectData();
+    ProjectIndex i = indexes.getSearchIndex();
+    if (i == null) {
+      return false; // No index; caller couldn't do anything if it is stale.
+    }
+
+    Optional<FieldBundle> result =
+        i.getRaw(project, QueryOptions.create(indexConfig, 0, 1, FIELDS));
+    if (!result.isPresent()) {
+      return true;
+    }
+
+    SetMultimap<Project.NameKey, RefState> indexedRefStates =
+        RefState.parseStates(result.get().getValue(ProjectField.REF_STATE));
+
+    SetMultimap<Project.NameKey, RefState> currentRefStates =
+        MultimapBuilder.hashKeys().hashSetValues().build();
+    projectData
+        .tree()
+        .stream()
+        .filter(p -> p.getProject().getConfigRefState() != null)
+        .forEach(
+            p ->
+                currentRefStates.put(
+                    p.getProject().getNameKey(),
+                    RefState.create(RefNames.REFS_CONFIG, p.getProject().getConfigRefState())));
+
+    return !currentRefStates.equals(indexedRefStates);
+  }
+}
diff --git a/java/com/google/gerrit/server/ioutil/HexFormat.java b/java/com/google/gerrit/server/ioutil/HexFormat.java
new file mode 100644
index 0000000..fd9c17a
--- /dev/null
+++ b/java/com/google/gerrit/server/ioutil/HexFormat.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ioutil;
+
+public class HexFormat {
+  public static String fromInt(int id) {
+    final char[] r = new char[8];
+    for (int p = 7; 0 <= p; p--) {
+      final int h = id & 0xf;
+      r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10));
+      id >>= 4;
+    }
+    return new String(r);
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
index 9032932..e18fd42 100644
--- a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
+++ b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailHeader;
+import com.google.gerrit.mail.MailMessage;
 import com.google.inject.Singleton;
 
 /** Filters out auto-reply messages according to RFC 3834. */
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index 5a41c77..eee8c60 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -17,8 +17,8 @@
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Arrays;
@@ -52,7 +52,7 @@
       return true;
     }
 
-    boolean match = mailPattern.matcher(message.from().email).find();
+    boolean match = mailPattern.matcher(message.from().getEmail()).find();
     if (mode == ListFilterMode.WHITELIST && !match || mode == ListFilterMode.BLACKLIST && match) {
       logger.atInfo().log("Mail message from %s rejected by list filter", message.from());
       return false;
diff --git a/java/com/google/gerrit/server/mail/MailFilter.java b/java/com/google/gerrit/server/mail/MailFilter.java
index d50064d..5fff8a3 100644
--- a/java/com/google/gerrit/server/mail/MailFilter.java
+++ b/java/com/google/gerrit/server/mail/MailFilter.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
 
 /**
  * Listener to filter incoming email.
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index 0487cc0..507b53f 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
-import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -34,8 +33,6 @@
 import org.eclipse.jgit.revwalk.FooterLine;
 
 public class MailUtil {
-  public static DateTimeFormatter rfcDateformatter =
-      DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ");
 
   public static MailRecipients getRecipientsFromFooters(
       AccountResolver accountResolver, List<FooterLine> footerLines)
diff --git a/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java b/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
index 169b41e..648006d 100644
--- a/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
@@ -15,6 +15,9 @@
 package com.google.gerrit.server.mail.receive;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.mail.MailParsingException;
+import com.google.gerrit.mail.RawMailParser;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gerrit.server.mail.Encryption;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index bf76bde..75053c8 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -26,6 +26,12 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.mail.HtmlParser;
+import com.google.gerrit.mail.MailComment;
+import com.google.gerrit.mail.MailHeaderParser;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.mail.MailMetadata;
+import com.google.gerrit.mail.TextParser;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -283,7 +289,7 @@
 
       comments = new ArrayList<>();
       for (MailComment c : parsedComments) {
-        if (c.type == MailComment.CommentType.CHANGE_MESSAGE) {
+        if (c.getType() == MailComment.CommentType.CHANGE_MESSAGE) {
           continue;
         }
         comments.add(
@@ -301,8 +307,8 @@
     @Override
     public void postUpdate(Context ctx) throws Exception {
       String patchSetComment = null;
-      if (parsedComments.get(0).type == MailComment.CommentType.CHANGE_MESSAGE) {
-        patchSetComment = parsedComments.get(0).message;
+      if (parsedComments.get(0).getType() == MailComment.CommentType.CHANGE_MESSAGE) {
+        patchSetComment = parsedComments.get(0).getMessage();
       }
       // Send email notifications
       outgoingMailFactory
@@ -342,12 +348,12 @@
 
     private ChangeMessage generateChangeMessage(ChangeContext ctx) {
       String changeMsg = "Patch Set " + psId.get() + ":";
-      if (parsedComments.get(0).type == MailComment.CommentType.CHANGE_MESSAGE) {
+      if (parsedComments.get(0).getType() == MailComment.CommentType.CHANGE_MESSAGE) {
         // Add a blank line after Patch Set to follow the default format
         if (parsedComments.size() > 1) {
           changeMsg += "\n\n" + numComments(parsedComments.size() - 1);
         }
-        changeMsg += "\n\n" + parsedComments.get(0).message;
+        changeMsg += "\n\n" + parsedComments.get(0).getMessage();
       } else {
         changeMsg += "\n\n" + numComments(parsedComments.size());
       }
@@ -356,11 +362,11 @@
 
     private PatchSet targetPatchSetForComment(
         ChangeContext ctx, MailComment mailComment, PatchSet current) throws OrmException {
-      if (mailComment.inReplyTo != null) {
+      if (mailComment.getInReplyTo() != null) {
         return psUtil.get(
             ctx.getDb(),
             ctx.getNotes(),
-            new PatchSet.Id(ctx.getChange().getId(), mailComment.inReplyTo.key.patchSetId));
+            new PatchSet.Id(ctx.getChange().getId(), mailComment.getInReplyTo().key.patchSetId));
       }
       return current;
     }
@@ -372,11 +378,11 @@
       // The patch set that this comment is based on is different if this
       // comment was sent in reply to a comment on a previous patch set.
       Side side;
-      if (mailComment.inReplyTo != null) {
-        fileName = mailComment.inReplyTo.key.filename;
-        side = Side.fromShort(mailComment.inReplyTo.side);
+      if (mailComment.getInReplyTo() != null) {
+        fileName = mailComment.getInReplyTo().key.filename;
+        side = Side.fromShort(mailComment.getInReplyTo().side);
       } else {
-        fileName = mailComment.fileName;
+        fileName = mailComment.getFileName();
         side = Side.REVISION;
       }
 
@@ -386,16 +392,16 @@
               fileName,
               patchSetForComment.getId(),
               (short) side.ordinal(),
-              mailComment.message,
+              mailComment.getMessage(),
               false,
               null);
 
       comment.tag = tag;
-      if (mailComment.inReplyTo != null) {
-        comment.parentUuid = mailComment.inReplyTo.key.uuid;
-        comment.lineNbr = mailComment.inReplyTo.lineNbr;
-        comment.range = mailComment.inReplyTo.range;
-        comment.unresolved = mailComment.inReplyTo.unresolved;
+      if (mailComment.getInReplyTo() != null) {
+        comment.parentUuid = mailComment.getInReplyTo().key.uuid;
+        comment.lineNbr = mailComment.getInReplyTo().lineNbr;
+        comment.range = mailComment.getInReplyTo().range;
+        comment.unresolved = mailComment.getInReplyTo().unresolved;
       }
       CommentsUtil.setCommentRevId(comment, patchListCache, ctx.getChange(), patchSetForComment);
       return comment;
diff --git a/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
index e4ad969..dc99b46 100644
--- a/java/com/google/gerrit/server/mail/receive/MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gerrit.server.update.UpdateException;
diff --git a/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
index a3ea265..54971c4 100644
--- a/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -16,6 +16,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.mail.MailParsingException;
+import com.google.gerrit.mail.RawMailParser;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gerrit.server.mail.Encryption;
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index ae8ac31..433bd9b 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountSshKey;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 503fbd0..00a496a2 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -30,7 +31,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchList;
@@ -403,12 +403,19 @@
 
   @Override
   protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
-    return projectState.statePermitsRead()
-        && args.permissionBackend
-            .absentUser(to)
-            .change(changeData)
-            .database(args.db)
-            .test(ChangePermission.READ);
+    if (!projectState.statePermitsRead()) {
+      return false;
+    }
+    try {
+      args.permissionBackend
+          .absentUser(to)
+          .change(changeData)
+          .database(args.db)
+          .check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
   }
 
   /** Find all users who are authors of any part of this change. */
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 0095fc1..54176e2 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -23,6 +23,8 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.mail.MailHeader;
+import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -32,8 +34,6 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.mail.MailHeader;
-import com.google.gerrit.server.mail.MailUtil;
 import com.google.gerrit.server.mail.receive.Protocol;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
@@ -566,7 +566,7 @@
 
   private String getCommentTimestamp() {
     // Grouping is currently done by timestamp.
-    return MailUtil.rfcDateformatter.format(
+    return MailProcessingUtil.rfcDateformatter.format(
         ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index c434d06..576d506 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -16,11 +16,11 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.mail.Address;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
diff --git a/java/com/google/gerrit/server/mail/send/EmailSender.java b/java/com/google/gerrit/server/mail/send/EmailSender.java
index 23fa1fe..ce4964d 100644
--- a/java/com/google/gerrit/server/mail/send/EmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/EmailSender.java
@@ -16,7 +16,8 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
 import java.util.Collection;
 import java.util.Map;
 
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
index 2489063..5baabe9 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.mail.Address;
 
 /** Constructs an address to send email from. */
 public interface FromAddressGenerator {
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
index 500eef3..b77909e 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -17,13 +17,13 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.MailUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 5143dc7..65ce128 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -18,8 +18,8 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MailHeader;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailHeader;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import org.apache.james.mime4j.dom.field.FieldName;
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 9f94fa3..f94f1ca 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 0cc7a1d..032bcbf 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -19,11 +19,11 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 0612237..fe9b446 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -26,12 +26,13 @@
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.mail.EmailHeader.AddressList;
+import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MailHeader;
-import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 15197ef..9a86fdb 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.git.NotifyConfig;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index c667026..0dbfb60 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -18,8 +18,8 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 10e8b0b..2262b5c 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -22,9 +22,10 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.Encryption;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 5f2593b..5810c64 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -55,6 +55,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -70,7 +71,6 @@
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.LabelVote;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 3eb06b2..3543202 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -39,6 +39,7 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -61,7 +62,6 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
 import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gson.Gson;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 69fbfe1..903a982 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -52,6 +52,7 @@
 import com.google.common.collect.TreeBasedTable;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -62,7 +63,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.RequestId;
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
index 9a9a211..b250a34 100644
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
@@ -14,20 +14,27 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.notedb.RevisionNote.MAX_NOTE_SZ;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.PackInserter;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -47,22 +54,65 @@
 public class CommentJsonMigrator {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  public static class ProjectMigrationResult {
+    public int skipped;
+    public boolean ok;
+    public List<String> refsUpdated;
+  }
+
   private final LegacyChangeNoteRead legacyChangeNoteRead;
   private final ChangeNoteJson changeNoteJson;
+  private final AllUsersName allUsers;
 
   @Inject
   CommentJsonMigrator(
-      ChangeNoteJson changeNoteJson, GerritServerIdProvider gerritServerIdProvider) {
+      ChangeNoteJson changeNoteJson,
+      GerritServerIdProvider gerritServerIdProvider,
+      AllUsersName allUsers) {
     this.changeNoteJson = changeNoteJson;
+    this.allUsers = allUsers;
     this.legacyChangeNoteRead = new LegacyChangeNoteRead(gerritServerIdProvider.get());
   }
 
-  CommentJsonMigrator(ChangeNoteJson changeNoteJson, String serverId) {
+  CommentJsonMigrator(ChangeNoteJson changeNoteJson, String serverId, AllUsersName allUsers) {
     this.changeNoteJson = changeNoteJson;
     this.legacyChangeNoteRead = new LegacyChangeNoteRead(serverId);
+    this.allUsers = allUsers;
   }
 
-  public boolean migrateChanges(
+  public ProjectMigrationResult migrateProject(
+      Project.NameKey project, Repository repo, boolean dryRun) {
+    ProjectMigrationResult progress = new ProjectMigrationResult();
+    progress.ok = true;
+    progress.skipped = 0;
+    progress.refsUpdated = ImmutableList.of();
+    try (RevWalk rw = new RevWalk(repo);
+        ObjectInserter ins = newPackInserter(repo)) {
+      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+      bru.setAllowNonFastForwards(true);
+      progress.ok &= migrateChanges(project, repo, rw, ins, bru);
+      if (project.equals(allUsers)) {
+        progress.ok &= migrateDrafts(allUsers, repo, rw, ins, bru);
+      }
+
+      progress.refsUpdated =
+          bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
+      if (!bru.getCommands().isEmpty()) {
+        if (!dryRun) {
+          ins.flush();
+          RefUpdateUtil.executeChecked(bru, rw);
+        }
+      } else {
+        progress.skipped++;
+      }
+    } catch (IOException e) {
+      progress.ok = false;
+    }
+
+    return progress;
+  }
+
+  private boolean migrateChanges(
       Project.NameKey project, Repository repo, RevWalk rw, ObjectInserter ins, BatchRefUpdate bru)
       throws IOException {
     boolean ok = true;
@@ -76,7 +126,7 @@
     return ok;
   }
 
-  public boolean migrateDrafts(
+  private boolean migrateDrafts(
       Project.NameKey allUsers,
       Repository allUsersRepo,
       RevWalk rw,
@@ -187,4 +237,13 @@
     rw.sort(RevSort.REVERSE);
     rw.markStart(rw.parseCommit(id));
   }
+
+  private static ObjectInserter newPackInserter(Repository repo) {
+    if (!(repo instanceof FileRepository)) {
+      return repo.newObjectInserter();
+    }
+    PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
+    ins.checkExisting(false);
+    return ins;
+  }
 }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 6f3e055..b4f7251 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -37,6 +37,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -172,6 +173,8 @@
       }
     }
 
+    correctForDifferencesInNewlineAtEnd();
+
     if (comments != null) {
       ensureCommentsVisible(comments);
     }
@@ -277,6 +280,43 @@
     }
   }
 
+  private void correctForDifferencesInNewlineAtEnd() {
+    // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
+    int aSize = a.src.size();
+    int bSize = b.src.size();
+
+    Optional<Edit> lastEdit = getLast(edits);
+    if (isNewlineAtEndDeleted()) {
+      Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
+      if (lastLineEdit.isPresent()) {
+        lastLineEdit.get().extendA();
+      } else {
+        Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
+        edits.add(newlineEdit);
+      }
+    } else if (isNewlineAtEndAdded()) {
+      Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
+      if (lastLineEdit.isPresent()) {
+        lastLineEdit.get().extendB();
+      } else {
+        Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
+        edits.add(newlineEdit);
+      }
+    }
+  }
+
+  private static <T> Optional<T> getLast(List<T> list) {
+    return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
+  }
+
+  private boolean isNewlineAtEndDeleted() {
+    return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
+  }
+
+  private boolean isNewlineAtEndAdded() {
+    return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
+  }
+
   private void ensureCommentsVisible(CommentDetail comments) {
     if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
       // No comments, no additional dummy edits are required.
@@ -396,14 +436,14 @@
     for (EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
         if (hunk.isContextLine()) {
-          final String lineA = a.src.getString(hunk.getCurA());
+          String lineA = a.getSourceLine(hunk.getCurA());
           a.dst.addLine(hunk.getCurA(), lineA);
 
           if (ignoredWhitespace) {
             // If we ignored whitespace in some form, also get the line
             // from b when it does not exactly match the line from a.
             //
-            final String lineB = b.src.getString(hunk.getCurB());
+            String lineB = b.getSourceLine(hunk.getCurB());
             if (!lineA.equals(lineB)) {
               b.dst.addLine(hunk.getCurB(), lineB);
             }
@@ -437,11 +477,22 @@
     final SparseFileContent dst = new SparseFileContent();
 
     int size() {
-      return src != null ? src.size() : 0;
+      if (src == null) {
+        return 0;
+      }
+      if (src.isMissingNewlineAtEnd()) {
+        return src.size();
+      }
+      return src.size() + 1;
     }
 
-    void addLine(int line) {
-      dst.addLine(line, src.getString(line));
+    void addLine(int lineNumber) {
+      String lineContent = getSourceLine(lineNumber);
+      dst.addLine(lineNumber, lineContent);
+    }
+
+    String getSourceLine(int lineNumber) {
+      return lineNumber >= src.size() ? "" : src.getString(lineNumber);
     }
 
     void resolve(Side other, ObjectId within) throws IOException {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index bf68026..136b4ae 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.cache.PerThreadCache;
-import com.google.gerrit.server.permissions.PermissionBackendCondition.WithUser;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 3b88080..1e5b3c8 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -299,9 +299,14 @@
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(db.get(), project)) {
         ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
-        if (projectState.statePermitsRead()
-            && permissionBackendForProject.indexedChange(cd, notes).test(ChangePermission.READ)) {
+        if (!projectState.statePermitsRead()) {
+          continue;
+        }
+        try {
+          permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ);
           visibleChanges.put(cd.getId(), cd.change().getDest());
+        } catch (AuthException e) {
+          // Do nothing.
         }
       }
       return visibleChanges;
@@ -334,11 +339,16 @@
           "Failed to load change %s in %s", r.id(), projectState.getName());
       return null;
     }
+
+    if (!projectState.statePermitsRead()) {
+      return null;
+    }
+
     try {
-      if (projectState.statePermitsRead()
-          && permissionBackendForProject.change(r.notes()).test(ChangePermission.READ)) {
-        return r.notes();
-      }
+      permissionBackendForProject.change(r.notes()).check(ChangePermission.READ);
+      return r.notes();
+    } catch (AuthException e) {
+      // Skip.
     } catch (PermissionBackendException e) {
       logger.atSevere().withCause(e).log(
           "Failed to check permission for %s in %s", r.id(), projectState.getName());
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 61a7ef2..79eccbb 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -68,6 +68,9 @@
           .put(
               BooleanProjectConfig.REJECT_EMPTY_COMMIT,
               new Mapper(i -> i.rejectEmptyCommit, (i, v) -> i.rejectEmptyCommit = v))
+          .put(
+              BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT,
+              new Mapper(i -> i.workInProgressByDefault, (i, v) -> i.workInProgressByDefault = v))
           .build();
 
   static {
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index d42b652..e3638b6 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -55,7 +56,6 @@
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
-import com.google.gerrit.server.mail.Address;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -402,10 +402,6 @@
     return mimeTypes;
   }
 
-  public GroupReference resolve(AccountGroup group) {
-    return resolve(GroupReference.forGroup(group));
-  }
-
   public GroupReference resolve(GroupReference group) {
     GroupReference groupRef = groupList.resolve(group);
     if (groupRef != null
@@ -503,6 +499,9 @@
     if (p.getDescription() == null) {
       p.setDescription("");
     }
+    if (revision != null) {
+      p.setConfigRefState(revision.toObjectId().name());
+    }
 
     if (rc.getStringList(ACCESS, null, KEY_INHERIT_FROM).length > 1) {
       // The config must not contain more than one parent to inherit from
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 87dbcfc..e36ca4f 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -62,6 +62,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Ref;
@@ -538,7 +539,11 @@
   }
 
   public ProjectData toProjectData() {
-    return new ProjectData(getProject(), parents().transform(s -> s.getProject().getNameKey()));
+    ProjectData project = null;
+    for (ProjectState state : treeInOrder()) {
+      project = new ProjectData(state.getProject(), Optional.ofNullable(project));
+    }
+    return project;
   }
 
   private String readFile(Path p) throws IOException {
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index e7ffd5e..41d53a2 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -19,6 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.LimitPredicate;
@@ -120,12 +121,17 @@
   public Predicate<AccountState> cansee(String change)
       throws QueryParseException, OrmException, PermissionBackendException {
     ChangeNotes changeNotes = args.changeFinder.findOne(change);
-    if (changeNotes == null
-        || !args.permissionBackend
-            .user(args.getUser())
-            .database(args.db)
-            .change(changeNotes)
-            .test(ChangePermission.READ)) {
+    if (changeNotes == null) {
+      throw error(String.format("change %s not found", change));
+    }
+
+    try {
+      args.permissionBackend
+          .user(args.getUser())
+          .database(args.db)
+          .change(changeNotes)
+          .check(ChangePermission.READ);
+    } catch (AuthException e) {
       throw error(String.format("change %s not found", change));
     }
 
@@ -218,7 +224,12 @@
   }
 
   private boolean canSeeSecondaryEmails() throws PermissionBackendException, QueryParseException {
-    return args.permissionBackend.user(args.getUser()).test(GlobalPermission.MODIFY_ACCOUNT);
+    try {
+      args.permissionBackend.user(args.getUser()).check(GlobalPermission.MODIFY_ACCOUNT);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
   }
 
   private boolean checkedCanSeeSecondaryEmails() {
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index b008092..fb3549c 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountState;
@@ -40,13 +41,16 @@
   @Override
   public boolean match(AccountState accountState) throws OrmException {
     try {
-      return permissionBackend
+      permissionBackend
           .absentUser(accountState.getAccount().getId())
           .database(db)
           .change(changeNotes)
-          .test(ChangePermission.READ);
+          .check(ChangePermission.READ);
+      return true;
     } catch (PermissionBackendException e) {
       throw new OrmException("Failed to check if account can see change", e);
+    } catch (AuthException e) {
+      return false;
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 346ac8e..27baef1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -83,13 +84,12 @@
       throw new OrmException("unable to read project state", e);
     }
 
-    boolean visible;
     PermissionBackend.WithUser withUser =
         user.isIdentifiedUser()
             ? permissionBackend.absentUser(user.getAccountId())
             : permissionBackend.user(anonymousUserProvider.get());
     try {
-      visible = withUser.indexedChange(cd, notes).database(db).test(ChangePermission.READ);
+      withUser.indexedChange(cd, notes).database(db).check(ChangePermission.READ);
     } catch (PermissionBackendException e) {
       Throwable cause = e.getCause();
       if (cause instanceof RepositoryNotFoundException) {
@@ -98,12 +98,12 @@
         return false;
       }
       throw new OrmException("unable to check permissions on change " + cd.getId(), e);
+    } catch (AuthException e) {
+      return false;
     }
-    if (visible) {
-      cd.cacheVisibleTo(user);
-      return true;
-    }
-    return false;
+
+    cd.cacheVisibleTo(user);
+    return true;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 3113504..5855404 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -65,7 +66,6 @@
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 19fd4a1..54e22f3 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -125,10 +126,13 @@
       PermissionBackend.ForChange perm =
           permissionBackend.absentUser(approver).database(dbProvider).change(cd);
       ProjectState projectState = projectCache.checkedGet(cd.project());
-      return projectState != null
-          && projectState.statePermitsRead()
-          && perm.test(ChangePermission.READ);
-    } catch (PermissionBackendException | IOException e) {
+      if (projectState == null || !projectState.statePermitsRead()) {
+        return false;
+      }
+
+      perm.check(ChangePermission.READ);
+      return true;
+    } catch (PermissionBackendException | IOException | AuthException e) {
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
index f4e979c..667c630 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -17,8 +17,8 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gwtorm.server.OrmException;
 
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index b4f56d4..2e406aa 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.project;
 
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.index.project.ProjectPredicate;
@@ -34,5 +35,9 @@
     return new ProjectPredicate(ProjectField.DESCRIPTION, description);
   }
 
+  public static Predicate<ProjectData> state(ProjectState state) {
+    return new ProjectPredicate(ProjectField.STATE, state.name());
+  }
+
   private ProjectPredicates() {}
 }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index be7ea22..872d3e0 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -60,6 +61,23 @@
     return ProjectPredicates.description(description);
   }
 
+  @Operator
+  public Predicate<ProjectData> state(String state) throws QueryParseException {
+    if (Strings.isNullOrEmpty(state)) {
+      throw error("state operator requires a value");
+    }
+    ProjectState parsedState;
+    try {
+      parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+    } catch (IllegalArgumentException e) {
+      throw error("state operator must be either 'active' or 'read-only'");
+    }
+    if (parsedState == ProjectState.HIDDEN) {
+      throw error("state operator must be either 'active' or 'read-only'");
+    }
+    return ProjectPredicates.state(parsedState);
+  }
+
   @Override
   protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
     // Adapt the capacity of this list when adding more default predicates.
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index ff114fae..1207c71 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -12,6 +12,7 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/reviewdb:server",
diff --git a/java/com/google/gerrit/server/restapi/account/Capabilities.java b/java/com/google/gerrit/server/restapi/account/Capabilities.java
index ec16e2b..85f9da0 100644
--- a/java/com/google/gerrit/server/restapi/account/Capabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/Capabilities.java
@@ -71,10 +71,12 @@
     }
 
     GlobalOrPluginPermission perm = parse(id);
-    if (permissionBackend.user(target).test(perm)) {
+    try {
+      permissionBackend.user(target).check(perm);
       return new AccountResource.Capability(target, globalOrPluginPermissionName(perm));
+    } catch (AuthException e) {
+      throw new ResourceNotFoundException(id);
     }
-    throw new ResourceNotFoundException(id);
   }
 
   private GlobalOrPluginPermission parse(IdString id) throws ResourceNotFoundException {
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index b1581abb..3e43082 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
@@ -100,13 +101,13 @@
   public Response<AccountInfo> apply(
       TopLevelResource rsrc, IdString id, @Nullable AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException {
+          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
     return apply(id, input != null ? input : new AccountInput());
   }
 
   public Response<AccountInfo> apply(IdString id, AccountInput input)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException,
-          OrmException, IOException, ConfigInvalidException {
+          OrmException, IOException, ConfigInvalidException, PermissionBackendException {
     String username = id.get();
     if (input.username != null && !username.equals(input.username)) {
       throw new BadRequestException("username must match URL");
diff --git a/java/com/google/gerrit/server/restapi/account/GetAccount.java b/java/com/google/gerrit/server/restapi/account/GetAccount.java
index 0d8e25e..6b73ae3b 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAccount.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -32,7 +33,7 @@
   }
 
   @Override
-  public AccountInfo apply(AccountResource rsrc) throws OrmException {
+  public AccountInfo apply(AccountResource rsrc) throws OrmException, PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getUser().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
index dced4d7..edcbc35 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.config.AgreementJson;
 import com.google.inject.Inject;
@@ -60,7 +61,8 @@
   }
 
   @Override
-  public List<AgreementInfo> apply(AccountResource resource) throws RestApiException {
+  public List<AgreementInfo> apply(AccountResource resource)
+      throws RestApiException, PermissionBackendException {
     if (!agreementsEnabled) {
       throw new MethodNotAllowedException("contributor agreements disabled");
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index c8f99b3..97d0c60 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -14,38 +14,25 @@
 
 package com.google.gerrit.server.restapi.account;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.common.AccountDetailInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.InternalAccountDirectory;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.Set;
 
 @Singleton
 public class GetDetail implements RestReadView<AccountResource> {
-  private final Provider<CurrentUser> self;
-  private final PermissionBackend permissionBackend;
   private final InternalAccountDirectory directory;
 
   @Inject
-  public GetDetail(
-      Provider<CurrentUser> self,
-      PermissionBackend permissionBackend,
-      InternalAccountDirectory directory) {
-    this.self = self;
-    this.permissionBackend = permissionBackend;
+  public GetDetail(InternalAccountDirectory directory) {
     this.directory = directory;
   }
 
@@ -56,13 +43,7 @@
     AccountDetailInfo info = new AccountDetailInfo(a.getId().get());
     info.registeredOn = a.getRegisteredOn();
     info.inactive = !a.isActive() ? true : null;
-    Set<FillOptions> fillOptions =
-        self.get().hasSameAccountId(rsrc.getUser())
-                || permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)
-            ? EnumSet.allOf(FillOptions.class)
-            : Sets.difference(
-                EnumSet.allOf(FillOptions.class), EnumSet.of(FillOptions.SECONDARY_EMAILS));
-    directory.fillAccountInfo(Collections.singleton(info), fillOptions);
+    directory.fillAccountInfo(Collections.singleton(info), EnumSet.allOf(FillOptions.class));
     return info;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
index a8d14f6..40201a8 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
@@ -50,7 +50,7 @@
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, ConfigInvalidException, IOException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 63d042c..85262ee 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -44,7 +44,7 @@
   @Override
   public List<EmailInfo> apply(AccountResource rsrc)
       throws AuthException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index 486a151..ad9746e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -42,7 +43,8 @@
   }
 
   @Override
-  public List<GroupInfo> apply(AccountResource resource) throws OrmException {
+  public List<GroupInfo> apply(AccountResource resource)
+      throws OrmException, PermissionBackendException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
     Set<AccountGroup.UUID> knownGroups = user.getEffectiveGroups().getKnownGroups();
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index 51d28ed..a828987 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -71,7 +71,7 @@
   public Response<String> apply(AccountResource.Email rsrc, Input input)
       throws RestApiException, OrmException, IOException, PermissionBackendException,
           ConfigInvalidException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 8784d23..2c0512c 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -171,9 +172,15 @@
       fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
       fillOptions.add(FillOptions.EMAIL);
 
-      if (modifyAccountCapabilityChecked
-          || permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
+      if (modifyAccountCapabilityChecked) {
         fillOptions.add(FillOptions.SECONDARY_EMAILS);
+      } else {
+        try {
+          permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+          fillOptions.add(FillOptions.SECONDARY_EMAILS);
+        } catch (AuthException e) {
+          // Do nothing.
+        }
       }
     }
     accountLoader = accountLoaderFactory.create(fillOptions);
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index 6aa88de..f4fa354 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -57,7 +57,7 @@
   public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo input)
       throws RestApiException, ConfigInvalidException, RepositoryNotFoundException, IOException,
           PermissionBackendException, OrmException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index dad6e0f..4e3f1d5 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -58,7 +58,7 @@
   public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo input)
       throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException, OrmException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index 11ecfdb..fccdabe 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -63,7 +63,7 @@
   public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo input)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
           OrmException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index e049539..5d4044c5 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -89,7 +89,7 @@
     return new RestReadView<AccountResource>() {
       @Override
       public Object apply(AccountResource self)
-          throws BadRequestException, AuthException, OrmException {
+          throws BadRequestException, AuthException, OrmException, PermissionBackendException {
         QueryChanges query = changes.list();
         query.addQuery("starredby:" + self.getUser().getAccountId().get());
         return query.apply(TopLevelResource.INSTANCE);
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index fb809ee..5c4c4d5 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -99,7 +99,7 @@
     @Override
     @SuppressWarnings("unchecked")
     public List<ChangeInfo> apply(AccountResource rsrc)
-        throws BadRequestException, AuthException, OrmException {
+        throws BadRequestException, AuthException, OrmException, PermissionBackendException {
       if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
index 251c66d..25fc350 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeMessages.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.change.ChangeMessageResource;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -51,7 +52,7 @@
 
   @Override
   public ChangeMessageResource parse(ChangeResource parent, IdString id)
-      throws OrmException, ResourceNotFoundException {
+      throws OrmException, ResourceNotFoundException, PermissionBackendException {
     String uuid = id.get();
 
     List<ChangeMessageInfo> changeMessages = listChangeMessages.apply(parent);
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index e90ce18..e92abe1 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.FixSuggestion;
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -70,7 +71,7 @@
   }
 
   private abstract class BaseCommentFormatter<F extends Comment, T extends CommentInfo> {
-    public T format(F comment) {
+    public T format(F comment) throws PermissionBackendException {
       AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null;
       T info = toInfo(comment, loader);
       if (loader != null) {
@@ -79,7 +80,7 @@
       return info;
     }
 
-    public Map<String, List<T>> format(Iterable<F> comments) {
+    public Map<String, List<T>> format(Iterable<F> comments) throws PermissionBackendException {
       AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null;
 
       Map<String, List<T>> out = new TreeMap<>();
@@ -105,7 +106,7 @@
       return out;
     }
 
-    public List<T> formatAsList(Iterable<F> comments) {
+    public List<T> formatAsList(Iterable<F> comments) throws PermissionBackendException {
       AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null;
 
       List<T> out =
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 0dcbeb0..a04716d 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -266,6 +266,12 @@
       AccountState accountState = me.state();
       GeneralPreferencesInfo info = accountState.getGeneralPreferences();
 
+      boolean isWorkInProgress =
+          input.workInProgress == null
+              ? rsrc.getProjectState().is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+                  || MoreObjects.firstNonNull(info.workInProgressByDefault, false)
+              : input.workInProgress;
+
       // Add a Change-Id line if there isn't already one
       String commitMessage = subject;
       if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
@@ -309,7 +315,7 @@
       }
       ins.setTopic(topic);
       ins.setPrivate(isPrivate);
-      ins.setWorkInProgress(input.workInProgress != null && input.workInProgress);
+      ins.setWorkInProgress(isWorkInProgress);
       ins.setGroups(groups);
       ins.setNotify(input.notify);
       ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index be689ca..c22fb1e 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -75,7 +76,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException {
+      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     if (Strings.isNullOrEmpty(in.path)) {
       throw new BadRequestException("path must be non-empty");
     } else if (in.message == null || in.message.trim().isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
index fac1003..3231d16 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
@@ -17,12 +17,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.NotifyUtil;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
diff --git a/java/com/google/gerrit/server/restapi/change/GetAssignee.java b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
index f78fae2..e95f8d8 100644
--- a/java/com/google/gerrit/server/restapi/change/GetAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -35,7 +36,8 @@
   }
 
   @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc) throws OrmException {
+  public Response<AccountInfo> apply(ChangeResource rsrc)
+      throws OrmException, PermissionBackendException {
     Optional<Account.Id> assignee = Optional.ofNullable(rsrc.getChange().getAssignee());
     if (assignee.isPresent()) {
       return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.get()));
diff --git a/java/com/google/gerrit/server/restapi/change/GetComment.java b/java/com/google/gerrit/server/restapi/change/GetComment.java
index b8db6a5..d067dff 100644
--- a/java/com/google/gerrit/server/restapi/change/GetComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetComment.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.CommentResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -33,7 +34,7 @@
   }
 
   @Override
-  public CommentInfo apply(CommentResource rsrc) throws OrmException {
+  public CommentInfo apply(CommentResource rsrc) throws OrmException, PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
index 787c93e..6049607 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDraftComment.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.DraftCommentResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -33,7 +34,8 @@
   }
 
   @Override
-  public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
+  public CommentInfo apply(DraftCommentResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson.get().newCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
index 354558b..279cfe3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -39,7 +40,8 @@
   }
 
   @Override
-  public Response<List<AccountInfo>> apply(ChangeResource rsrc) throws OrmException {
+  public Response<List<AccountInfo>> apply(ChangeResource rsrc)
+      throws OrmException, PermissionBackendException {
 
     Set<Account.Id> pastAssignees = rsrc.getNotes().load().getPastAssignees();
     if (pastAssignees == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
index bd1f66a..0197068 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RobotCommentResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -33,7 +34,8 @@
   }
 
   @Override
-  public RobotCommentInfo apply(RobotCommentResource rsrc) throws OrmException {
+  public RobotCommentInfo apply(RobotCommentResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson.get().newRobotCommentFormatter().format(rsrc.getComment());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index 37dc207..40f4642 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -49,7 +50,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index d7a102a..a524f6d 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -50,7 +51,7 @@
 
   @Override
   public Map<String, List<CommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, PermissionBackendException {
     if (!rsrc.getUser().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
index cf76ef1..39c12f7 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeMessages.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,7 +48,8 @@
   }
 
   @Override
-  public List<ChangeMessageInfo> apply(ChangeResource resource) throws OrmException {
+  public List<ChangeMessageInfo> apply(ChangeResource resource)
+      throws OrmException, PermissionBackendException {
     List<ChangeMessage> messages =
         changeMessagesUtil.byChange(dbProvider.get(), resource.getNotes());
     List<ChangeMessageInfo> messageInfos =
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index dd8de6f..dc92ced 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -47,7 +48,7 @@
 
   @Override
   public Map<String, List<RobotCommentInfo>> apply(ChangeResource rsrc)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, PermissionBackendException {
     ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 750e74f..46bb33f 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -16,12 +16,12 @@
 
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
index b7dc553..db8ef0c 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -51,7 +52,8 @@
   }
 
   @Override
-  public Map<String, List<CommentInfo>> apply(RevisionResource rsrc) throws OrmException {
+  public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
@@ -59,7 +61,8 @@
         .format(listComments(rsrc));
   }
 
-  public List<CommentInfo> getComments(RevisionResource rsrc) throws OrmException {
+  public List<CommentInfo> getComments(RevisionResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(includeAuthorInfo())
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index d0630b7..32c4ea3 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -17,12 +17,12 @@
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 61219d3..66138ab 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -42,7 +43,8 @@
   }
 
   @Override
-  public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc) throws OrmException {
+  public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
@@ -50,7 +52,8 @@
         .format(listComments(rsrc));
   }
 
-  public List<RobotCommentInfo> getComments(RevisionResource rsrc) throws OrmException {
+  public List<RobotCommentInfo> getComments(RevisionResource rsrc)
+      throws OrmException, PermissionBackendException {
     return commentJson
         .get()
         .setFillAccounts(true)
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 622e06b..20f3d8a 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -64,6 +64,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -94,7 +95,6 @@
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.CommentAdded;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 87e6163..0df86d1 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
@@ -56,7 +57,6 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -349,14 +349,17 @@
       NotifyHandling notify,
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
       throws PermissionBackendException {
-    if (!permissionBackend
-        .user(anonymousProvider.get())
-        .change(rsrc.getNotes())
-        .database(dbProvider)
-        .test(ChangePermission.READ)) {
+    try {
+      permissionBackend
+          .user(anonymousProvider.get())
+          .change(rsrc.getNotes())
+          .database(dbProvider)
+          .check(ChangePermission.READ);
+    } catch (AuthException e) {
       return fail(
           reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
     }
+
     if (!migration.readChanges()) {
       // addByEmail depends on NoteDb.
       return fail(
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
index 3cb6136..08b66aa 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -42,7 +43,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.extensions.events.ReviewerAdded;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.AddReviewerSender;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index e6ede34..76ef106 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -80,7 +81,7 @@
   @Override
   protected Response<CommentInfo> applyImpl(
       BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in)
-      throws RestApiException, UpdateException, OrmException {
+      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
     if (in == null || in.message == null || in.message.trim().isEmpty()) {
       return delete.applyImpl(updateFactory, rsrc, null);
     } else if (in.id != null && !rsrc.getId().equals(in.id)) {
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 1c9e420..b53c4e6 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
@@ -104,7 +105,7 @@
 
   @Override
   public List<?> apply(TopLevelResource rsrc)
-      throws BadRequestException, AuthException, OrmException {
+      throws BadRequestException, AuthException, OrmException, PermissionBackendException {
     List<List<ChangeInfo>> out;
     try {
       out = query();
@@ -117,7 +118,8 @@
     return out.size() == 1 ? out.get(0) : out;
   }
 
-  private List<List<ChangeInfo>> query() throws OrmException, QueryParseException {
+  private List<List<ChangeInfo>> query()
+      throws OrmException, QueryParseException, PermissionBackendException {
     if (imp.isDisabled()) {
       throw new QueryParseException("query disabled");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
index c891a65..29c5649 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -132,10 +133,15 @@
         for (SubmitRecord.Label label : rec.labels) {
           String name = label.label;
           LabelType type = labelTypes.byLabel(name);
-          if (!out.approvals.containsKey(name)
-              && type != null
-              && perm.test(new LabelPermission(type))) {
+          if (out.approvals.containsKey(name) || type == null) {
+            continue;
+          }
+
+          try {
+            perm.check(new LabelPermission(type));
             out.approvals.put(name, formatValue((short) 0));
+          } catch (AuthException e) {
+            // Do nothing.
           }
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index a4cfbd2..f0aef13 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -21,12 +21,12 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 779ee5a..57ff0a3 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
@@ -48,8 +49,6 @@
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectState;
@@ -129,7 +128,6 @@
   private final IndexConfig indexConfig;
   private final AccountControl.Factory accountControlFactory;
   private final Provider<CurrentUser> self;
-  private final PermissionBackend permissionBackend;
 
   @Inject
   ReviewersUtil(
@@ -142,8 +140,7 @@
       AccountIndexCollection accountIndexes,
       IndexConfig indexConfig,
       AccountControl.Factory accountControlFactory,
-      Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      Provider<CurrentUser> self) {
     this.accountLoaderFactory = accountLoaderFactory;
     this.accountQueryBuilder = accountQueryBuilder;
     this.groupBackend = groupBackend;
@@ -154,7 +151,6 @@
     this.indexConfig = indexConfig;
     this.accountControlFactory = accountControlFactory;
     this.self = self;
-    this.permissionBackend = permissionBackend;
   }
 
   public interface VisibilityControl {
@@ -301,10 +297,7 @@
   private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
       throws PermissionBackendException {
     Set<FillOptions> fillOptions =
-        permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)
-            ? EnumSet.of(FillOptions.SECONDARY_EMAILS)
-            : EnumSet.noneOf(FillOptions.class);
-    fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+        Sets.union(AccountLoader.DETAILED_OPTIONS, EnumSet.of(FillOptions.SECONDARY_EMAILS));
     AccountLoader accountLoader = accountLoaderFactory.create(fillOptions);
 
     try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index 7cf30e2..b9b7a4f 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -22,12 +22,12 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 8fe5612..faf946f 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -65,16 +65,28 @@
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    Change change = rsrc.getChange();
-    if (!rsrc.isUserOwner()
-        && !permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)
-        && !permissionBackend
-            .currentUser()
-            .project(rsrc.getProject())
-            .test(ProjectPermission.WRITE_CONFIG)) {
-      throw new AuthException("not allowed to set ready for review");
+    if (!rsrc.isUserOwner()) {
+      boolean hasAdministrateServerPermission = false;
+      try {
+        permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+        hasAdministrateServerPermission = true;
+      } catch (AuthException e) {
+        // Skip.
+      }
+
+      if (!hasAdministrateServerPermission) {
+        try {
+          permissionBackend
+              .currentUser()
+              .project(rsrc.getProject())
+              .check(ProjectPermission.WRITE_CONFIG);
+        } catch (AuthException exp) {
+          throw new AuthException("not allowed to set ready for review");
+        }
+      }
     }
 
+    Change change = rsrc.getChange();
     if (change.getStatus() != Status.NEW) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index 9524903..1f9d81f 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -65,17 +65,28 @@
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    Change change = rsrc.getChange();
+    if (!rsrc.isUserOwner()) {
+      boolean hasAdministrateServerPermission = false;
+      try {
+        permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+        hasAdministrateServerPermission = true;
+      } catch (AuthException e) {
+        // Skip.
+      }
 
-    if (!rsrc.isUserOwner()
-        && !permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)
-        && !permissionBackend
-            .currentUser()
-            .project(rsrc.getProject())
-            .test(ProjectPermission.WRITE_CONFIG)) {
-      throw new AuthException("not allowed to set work in progress");
+      if (!hasAdministrateServerPermission) {
+        try {
+          permissionBackend
+              .currentUser()
+              .project(rsrc.getProject())
+              .check(ProjectPermission.WRITE_CONFIG);
+        } catch (AuthException exp) {
+          throw new AuthException("not allowed to set work in progress");
+        }
+      }
     }
 
+    Change change = rsrc.getChange();
     if (change.getStatus() != Status.NEW) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index 2a18612..cdd7426 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -63,7 +64,7 @@
 
   @Override
   public List<Record> apply(RevisionResource rsrc, TestSubmitRuleInput input)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, PermissionBackendException {
     if (input == null) {
       input = new TestSubmitRuleInput();
     }
diff --git a/java/com/google/gerrit/server/restapi/config/AgreementJson.java b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
index 548bc03..02e5f68 100644
--- a/java/com/google/gerrit/server/restapi/config/AgreementJson.java
+++ b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -48,7 +49,7 @@
     this.groupJson = groupJson;
   }
 
-  public AgreementInfo format(ContributorAgreement ca) {
+  public AgreementInfo format(ContributorAgreement ca) throws PermissionBackendException {
     AgreementInfo info = new AgreementInfo();
     info.name = ca.getName();
     info.description = ca.getDescription();
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 70db0f9..b7931bd 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -59,6 +59,7 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.restapi.change.AllowedFormats;
 import com.google.gerrit.server.submit.MergeSuperSet;
@@ -148,7 +149,8 @@
   }
 
   @Override
-  public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
+  public ServerInfo apply(ConfigResource rsrc)
+      throws MalformedURLException, PermissionBackendException {
     ServerInfo info = new ServerInfo();
     info.accounts = getAccountsInfo(accountVisibilityProvider);
     info.auth = getAuthInfo(authConfig, realm);
@@ -178,7 +180,7 @@
     return info;
   }
 
-  private AuthInfo getAuthInfo(AuthConfig cfg, Realm realm) {
+  private AuthInfo getAuthInfo(AuthConfig cfg, Realm realm) throws PermissionBackendException {
     AuthInfo info = new AuthInfo();
     info.authType = cfg.getAuthType();
     info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index d700028..7b69831 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -23,13 +23,13 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -133,7 +133,7 @@
     public String queueName;
 
     public TaskInfo(Task<?> task) {
-      this.id = IdGenerator.format(task.getTaskId());
+      this.id = HexFormat.fromInt(task.getTaskId());
       this.state = task.getState();
       this.startTime = new Timestamp(task.getStartTime().getTime());
       this.delay = task.getDelay(TimeUnit.MILLISECONDS);
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 3e2d1e7..733b6bd 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.group.MemberResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.gerrit.server.restapi.group.AddMembers.Input;
 import com.google.gwtorm.server.OrmException;
@@ -116,7 +117,8 @@
   @Override
   public List<AccountInfo> apply(GroupResource resource, Input input)
       throws AuthException, NotInternalGroupException, UnprocessableEntityException, OrmException,
-          IOException, ConfigInvalidException, ResourceNotFoundException {
+          IOException, ConfigInvalidException, ResourceNotFoundException,
+          PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -203,7 +205,8 @@
     }
   }
 
-  private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds) {
+  private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds)
+      throws PermissionBackendException {
     List<AccountInfo> result = new ArrayList<>();
     AccountLoader loader = infoFactory.create(true);
     for (Account.Id accId : accountIds) {
@@ -224,7 +227,7 @@
     @Override
     public AccountInfo apply(GroupResource resource, IdString id, Input input)
         throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException {
+            IOException, ConfigInvalidException, PermissionBackendException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id.get();
       try {
@@ -249,7 +252,8 @@
     }
 
     @Override
-    public AccountInfo apply(MemberResource resource, Input input) throws OrmException {
+    public AccountInfo apply(MemberResource resource, Input input)
+        throws OrmException, PermissionBackendException {
       // Do nothing, the user is already a member.
       return get.apply(resource);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index 21b6981..1474e19 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.group.SubgroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddSubgroups.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -92,7 +93,8 @@
   @Override
   public List<GroupInfo> apply(GroupResource resource, Input input)
       throws NotInternalGroupException, AuthException, UnprocessableEntityException, OrmException,
-          ResourceNotFoundException, IOException, ConfigInvalidException {
+          ResourceNotFoundException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     input = Input.init(input);
@@ -141,7 +143,7 @@
     @Override
     public GroupInfo apply(GroupResource resource, IdString id, Input input)
         throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-            IOException, ConfigInvalidException {
+            IOException, ConfigInvalidException, PermissionBackendException {
       AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(id.get());
       try {
@@ -166,7 +168,8 @@
     }
 
     @Override
-    public GroupInfo apply(SubgroupResource resource, Input input) throws OrmException {
+    public GroupInfo apply(SubgroupResource resource, Input input)
+        throws OrmException, PermissionBackendException {
       // Do nothing, the group is already included.
       return get.get().apply(resource);
     }
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 6ecb5aa..1014a99 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -121,7 +122,7 @@
   public GroupInfo apply(TopLevelResource resource, IdString id, GroupInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
           ResourceConflictException, OrmException, IOException, ConfigInvalidException,
-          ResourceNotFoundException {
+          ResourceNotFoundException, PermissionBackendException {
     String name = id.get();
     if (input == null) {
       input = new GroupInput();
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index eb66a37..7af4284 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -77,7 +78,7 @@
   @Override
   public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
       throws AuthException, NotInternalGroupException, OrmException, IOException,
-          ConfigInvalidException {
+          ConfigInvalidException, PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!rsrc.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/GetDetail.java b/java/com/google/gerrit/server/restapi/group/GetDetail.java
index e7b240e..75d1e34 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDetail.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -32,7 +33,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource rsrc) throws OrmException {
+  public GroupInfo apply(GroupResource rsrc) throws OrmException, PermissionBackendException {
     return json.format(rsrc);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetGroup.java b/java/com/google/gerrit/server/restapi/group/GetGroup.java
index 81057fd..c6cddb6 100644
--- a/java/com/google/gerrit/server/restapi/group/GetGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetGroup.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -31,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(GroupResource resource) throws OrmException {
+  public GroupInfo apply(GroupResource resource) throws OrmException, PermissionBackendException {
     return json.format(resource.getGroup());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GetMember.java b/java/com/google/gerrit/server/restapi/group/GetMember.java
index db33785..95063de 100644
--- a/java/com/google/gerrit/server/restapi/group/GetMember.java
+++ b/java/com/google/gerrit/server/restapi/group/GetMember.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.group.MemberResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -32,7 +33,7 @@
   }
 
   @Override
-  public AccountInfo apply(MemberResource rsrc) throws OrmException {
+  public AccountInfo apply(MemberResource rsrc) throws OrmException, PermissionBackendException {
     AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getMember().getAccountId());
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/group/GetOwner.java b/java/com/google/gerrit/server/restapi/group/GetOwner.java
index be19a24..0906ce6 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOwner.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -39,7 +40,8 @@
 
   @Override
   public GroupInfo apply(GroupResource resource)
-      throws NotInternalGroupException, ResourceNotFoundException, OrmException {
+      throws NotInternalGroupException, ResourceNotFoundException, OrmException,
+          PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     try {
diff --git a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
index 98e6ce5..16e2739 100644
--- a/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
+++ b/java/com/google/gerrit/server/restapi/group/GetSubgroup.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.SubgroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -31,7 +32,7 @@
   }
 
   @Override
-  public GroupInfo apply(SubgroupResource rsrc) throws OrmException {
+  public GroupInfo apply(SubgroupResource rsrc) throws OrmException, PermissionBackendException {
     return json.format(rsrc.getMemberDescription());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index 3c7799b..a51fad2 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -75,17 +76,18 @@
     return this;
   }
 
-  public GroupInfo format(GroupResource rsrc) throws OrmException {
+  public GroupInfo format(GroupResource rsrc) throws OrmException, PermissionBackendException {
     return createGroupInfo(rsrc.getGroup(), rsrc::getControl);
   }
 
-  public GroupInfo format(GroupDescription.Basic group) throws OrmException {
+  public GroupInfo format(GroupDescription.Basic group)
+      throws OrmException, PermissionBackendException {
     return createGroupInfo(group, Suppliers.memoize(() -> groupControlFactory.controlFor(group)));
   }
 
   private GroupInfo createGroupInfo(
       GroupDescription.Basic group, Supplier<GroupControl> groupControlSupplier)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
     GroupInfo info = createBasicGroupInfo(group);
 
     if (group instanceof GroupDescription.Internal) {
@@ -108,7 +110,7 @@
       GroupInfo info,
       GroupDescription.Internal internalGroup,
       Supplier<GroupControl> groupControlSupplier)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
     info.description = Strings.emptyToNull(internalGroup.getDescription());
     info.groupId = internalGroup.getId().get();
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index c7f1d5e..bae5eff 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.account.GetGroups;
 import com.google.gwtorm.server.OrmException;
@@ -246,7 +247,8 @@
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
@@ -256,7 +258,8 @@
   }
 
   public List<GroupInfo> get()
-      throws OrmException, RestApiException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (!Strings.isNullOrEmpty(suggest)) {
       return suggestGroups();
     }
@@ -280,7 +283,8 @@
     return getAllGroups();
   }
 
-  private List<GroupInfo> getAllGroups() throws OrmException, IOException, ConfigInvalidException {
+  private List<GroupInfo> getAllGroups()
+      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<GroupDescription.Internal> existingGroups =
         getAllExistingGroups()
@@ -312,7 +316,8 @@
     return groups.getAllGroupReferences();
   }
 
-  private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
+  private List<GroupInfo> suggestGroups()
+      throws OrmException, BadRequestException, PermissionBackendException {
     if (conflictingSuggestParameters()) {
       throw new BadRequestException(
           "You should only have no more than one --project and -n with --suggest");
@@ -368,7 +373,7 @@
   }
 
   private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
     Pattern pattern = getRegexPattern();
     Stream<? extends GroupDescription.Internal> foundGroups =
         groups
@@ -396,13 +401,14 @@
   }
 
   private List<GroupInfo> getGroupsOwnedBy(String id)
-      throws OrmException, RestApiException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     String uuid = groupsCollection.parse(id).getGroupUUID().get();
     return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
   }
 
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
     return filterGroupsOwnedBy(group -> isOwner(user, group));
   }
 
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 634e029..4742644 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
@@ -66,7 +67,7 @@
 
   @Override
   public List<AccountInfo> apply(GroupResource resource)
-      throws NotInternalGroupException, OrmException {
+      throws NotInternalGroupException, OrmException, PermissionBackendException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (recursive) {
@@ -75,7 +76,8 @@
     return getDirectMembers(group, resource.getControl());
   }
 
-  public List<AccountInfo> getTransitiveMembers(AccountGroup.UUID groupUuid) {
+  public List<AccountInfo> getTransitiveMembers(AccountGroup.UUID groupUuid)
+      throws PermissionBackendException {
     Optional<InternalGroup> group = groupCache.get(groupUuid);
     if (group.isPresent()) {
       InternalGroupDescription internalGroup = new InternalGroupDescription(group.get());
@@ -86,7 +88,8 @@
   }
 
   private List<AccountInfo> getTransitiveMembers(
-      GroupDescription.Internal group, GroupControl groupControl) {
+      GroupDescription.Internal group, GroupControl groupControl)
+      throws PermissionBackendException {
     checkSameGroup(group, groupControl);
     Set<Account.Id> members =
         getTransitiveMemberIds(
@@ -94,19 +97,21 @@
     return toAccountInfos(members);
   }
 
-  public List<AccountInfo> getDirectMembers(InternalGroup group) {
+  public List<AccountInfo> getDirectMembers(InternalGroup group) throws PermissionBackendException {
     InternalGroupDescription internalGroup = new InternalGroupDescription(group);
     return getDirectMembers(internalGroup, groupControlFactory.controlFor(internalGroup));
   }
 
   public List<AccountInfo> getDirectMembers(
-      GroupDescription.Internal group, GroupControl groupControl) {
+      GroupDescription.Internal group, GroupControl groupControl)
+      throws PermissionBackendException {
     checkSameGroup(group, groupControl);
     Set<Account.Id> directMembers = getDirectMemberIds(group, groupControl);
     return toAccountInfos(directMembers);
   }
 
-  private List<AccountInfo> toAccountInfos(Set<Account.Id> members) {
+  private List<AccountInfo> toAccountInfos(Set<Account.Id> members)
+      throws PermissionBackendException {
     List<AccountInfo> memberInfos = new ArrayList<>(members.size());
     for (Account.Id member : members) {
       memberInfos.add(accountLoader.get(member));
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index 835a613..97a260e 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -46,7 +47,8 @@
   }
 
   @Override
-  public List<GroupInfo> apply(GroupResource rsrc) throws NotInternalGroupException, OrmException {
+  public List<GroupInfo> apply(GroupResource rsrc)
+      throws NotInternalGroupException, OrmException, PermissionBackendException {
     GroupDescription.Internal group =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
 
@@ -54,7 +56,8 @@
   }
 
   public List<GroupInfo> getDirectSubgroups(
-      GroupDescription.Internal group, GroupControl groupControl) throws OrmException {
+      GroupDescription.Internal group, GroupControl groupControl)
+      throws OrmException, PermissionBackendException {
     boolean ownerOfParent = groupControl.isOwner();
     List<GroupInfo> included = new ArrayList<>();
     for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 5e7563e..56f856d 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -56,7 +57,7 @@
   public GroupInfo apply(GroupResource resource, OwnerInput input)
       throws ResourceNotFoundException, NotInternalGroupException, AuthException,
           BadRequestException, UnprocessableEntityException, OrmException, IOException,
-          ConfigInvalidException {
+          ConfigInvalidException, PermissionBackendException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
     if (!resource.getControl().isOwner()) {
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index c262003..fa9285d 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.group.GroupQueryBuilder;
 import com.google.gerrit.server.query.group.GroupQueryProcessor;
 import com.google.gwtorm.server.OrmException;
@@ -94,7 +95,8 @@
 
   @Override
   public List<GroupInfo> apply(TopLevelResource resource)
-      throws BadRequestException, MethodNotAllowedException, OrmException {
+      throws BadRequestException, MethodNotAllowedException, OrmException,
+          PermissionBackendException {
     if (Strings.isNullOrEmpty(query)) {
       throw new BadRequestException("missing query field");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index ea1620b..3b7f3e3 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -29,9 +29,9 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.restapi.project.GarbageCollect.Input;
-import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -98,7 +98,7 @@
     WorkQueue.Task<Void> task = (WorkQueue.Task<Void>) workQueue.getDefaultQueue().submit(job);
 
     String location =
-        canonicalUrl.get() + "a/config/server/tasks/" + IdGenerator.format(task.getTaskId());
+        canonicalUrl.get() + "a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId());
 
     return Response.accepted(location);
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 6a50c2f..d545f92 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -237,11 +237,15 @@
       }
     }
 
-    if (info.ownerOf.isEmpty()
-        && permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
-      // Special case: If the section list is empty, this project has no current
-      // access control information. Fall back to site administrators.
-      info.ownerOf.add(AccessSection.ALL);
+    if (info.ownerOf.isEmpty()) {
+      try {
+        permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+        // Special case: If the section list is empty, this project has no current
+        // access control information. Fall back to site administrators.
+        info.ownerOf.add(AccessSection.ALL);
+      } catch (AuthException e) {
+        // Do nothing.
+      }
     }
 
     if (config.getRevision() != null) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 0bdf979..bf4a547 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -185,9 +186,13 @@
         // showing the resolved value, show the name it references.
         //
         String target = ref.getTarget().getName();
-        if (!perm.ref(target).test(RefPermission.READ)) {
+
+        try {
+          perm.ref(target).check(RefPermission.READ);
+        } catch (AuthException e) {
           continue;
         }
+
         if (target.startsWith(Constants.R_HEADS)) {
           target = target.substring(Constants.R_HEADS.length());
         }
@@ -212,10 +217,13 @@
         continue;
       }
 
-      if (perm.ref(ref.getName()).test(RefPermission.READ)) {
+      try {
+        perm.ref(ref.getName()).check(RefPermission.READ);
         branches.add(
             createBranchInfo(
                 perm.ref(ref.getName()), ref, rsrc.getProjectState(), rsrc.getUser(), targets));
+      } catch (AuthException e) {
+        // Do nothing.
       }
     }
     Collections.sort(branches, new BranchComparator());
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 4ff46cf..06dbdb0 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -18,6 +18,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -104,8 +105,15 @@
         RevWalk rw = new RevWalk(git)) {
       List<DashboardInfo> all = new ArrayList<>();
       for (Ref ref : git.getRefDatabase().getRefsByPrefix(REFS_DASHBOARDS)) {
-        if (perm.ref(ref.getName()).test(RefPermission.READ) && state.statePermitsRead()) {
+        if (!state.statePermitsRead()) {
+          continue;
+        }
+
+        try {
+          perm.ref(ref.getName()).check(RefPermission.READ);
           all.addAll(scanDashboards(state.getProject(), git, rw, ref, project, setDefault));
+        } catch (AuthException e) {
+          // Do nothing.
         }
       }
       return all;
diff --git a/java/com/google/gerrit/server/schema/Schema_169.java b/java/com/google/gerrit/server/schema/Schema_169.java
index 11aebdd..2779d47 100644
--- a/java/com/google/gerrit/server/schema/Schema_169.java
+++ b/java/com/google/gerrit/server/schema/Schema_169.java
@@ -18,32 +18,25 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.CommentJsonMigrator;
+import com.google.gerrit.server.notedb.CommentJsonMigrator.ProjectMigrationResult;
 import com.google.gerrit.server.notedb.MutableNotesMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.SortedSet;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.storage.file.PackInserter;
-import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Migrate NoteDb inline comments to JSON format. */
 public class Schema_169 extends SchemaVersion {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private final AllUsersName allUsers;
   private final CommentJsonMigrator migrator;
   private final GitRepositoryManager repoManager;
   private final NotesMigration notesMigration;
@@ -51,12 +44,10 @@
   @Inject
   Schema_169(
       Provider<Schema_168> prior,
-      AllUsersName allUsers,
       CommentJsonMigrator migrator,
       GitRepositoryManager repoManager,
       @GerritServerConfig Config config) {
     super(prior);
-    this.allUsers = allUsers;
     this.migrator = migrator;
     this.repoManager = repoManager;
     this.notesMigration = MutableNotesMigration.fromConfig(config);
@@ -67,15 +58,6 @@
     migrateData(ui);
   }
 
-  private static ObjectInserter newPackInserter(Repository repo) {
-    if (!(repo instanceof FileRepository)) {
-      return repo.newObjectInserter();
-    }
-    PackInserter ins = ((FileRepository) repo).getObjectDatabase().newPackInserter();
-    ins.checkExisting(false);
-    return ins;
-  }
-
   @VisibleForTesting
   protected void migrateData(UpdateUI ui) throws OrmException {
     //  If the migration hasn't started, no need to look for non-JSON
@@ -89,28 +71,16 @@
     pm.beginTask("Migrating projects", projects.size());
     int skipped = 0;
     for (Project.NameKey project : projects) {
-      try (Repository repo = repoManager.openRepository(project);
-          RevWalk rw = new RevWalk(repo);
-          ObjectInserter ins = newPackInserter(repo)) {
-        BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-        bru.setAllowNonFastForwards(true);
-        ok &= migrator.migrateChanges(project, repo, rw, ins, bru);
-        if (project.equals(allUsers)) {
-          ok &= migrator.migrateDrafts(allUsers, repo, rw, ins, bru);
-        }
-
-        if (!bru.getCommands().isEmpty()) {
-          ins.flush();
-          RefUpdateUtil.executeChecked(bru, rw);
-        } else {
-          skipped++;
-        }
+      try (Repository repo = repoManager.openRepository(project)) {
+        ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
+        skipped += progress.skipped;
       } catch (IOException e) {
-        logger.atWarning().log("Error migrating project " + project, e);
         ok = false;
+        logger.atWarning().log("Error migrating project " + project, e);
       }
       pm.update(1);
     }
+
     pm.endTask();
     ui.message(
         "Skipped " + skipped + " project" + (skipped == 1 ? "" : "s") + " with no legacy comments");
diff --git a/java/com/google/gerrit/server/submit/GitModules.java b/java/com/google/gerrit/server/submit/GitModules.java
index 00ce7b2..bf56fd5 100644
--- a/java/com/google/gerrit/server/submit/GitModules.java
+++ b/java/com/google/gerrit/server/submit/GitModules.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.util.RequestId;
-import com.google.gerrit.server.util.SubmoduleSectionParser;
+import com.google.gerrit.server.util.git.SubmoduleSectionParser;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 06f57b5..9efb976 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -182,14 +183,19 @@
         changeSet.ids().contains(cd.getId())
             && (projectState != null)
             && projectState.statePermitsRead();
-    if (visible
-        && !permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ)) {
+    if (!visible) {
+      return false;
+    }
+
+    try {
+      permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
       // We thought the change was visible, but it isn't.
       // This can happen if the ACL changes during the
       // completeChangeSet computation, for example.
-      visible = false;
+      return false;
     }
-    return visible;
   }
 
   private SubmitType submitType(ChangeData cd) throws OrmException {
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index 3e9f068..31bcc2a 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -100,17 +101,22 @@
       List<ChangeData> cds = queryProvider.get().byLegacyChangeId(change.getId());
       checkState(cds.size() == 1, "Expected exactly one ChangeData, got " + cds.size());
       ChangeData cd = Iterables.getFirst(cds, null);
-      ProjectState projectState = projectCache.checkedGet(cd.project());
-      ChangeSet changeSet =
-          new ChangeSet(
-              cd,
-              projectState != null
-                  && projectState.statePermitsRead()
-                  && permissionBackend
-                      .user(user)
-                      .change(cd)
-                      .database(db)
-                      .test(ChangePermission.READ));
+
+      boolean visible = false;
+      if (cd != null) {
+        ProjectState projectState = projectCache.checkedGet(cd.project());
+
+        if (projectState.statePermitsRead()) {
+          try {
+            permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+            visible = true;
+          } catch (AuthException e) {
+            // Do nothing.
+          }
+        }
+      }
+
+      ChangeSet changeSet = new ChangeSet(cd, visible);
       if (wholeTopicEnabled(cfg)) {
         return completeChangeSetIncludingTopics(db, changeSet, user);
       }
@@ -203,8 +209,15 @@
   private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
       throws PermissionBackendException, IOException {
     ProjectState projectState = projectCache.checkedGet(cd.project());
-    return projectState != null
-        && projectState.statePermitsRead()
-        && permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ);
+    if (projectState == null || !projectState.statePermitsRead()) {
+      return false;
+    }
+
+    try {
+      permissionBackend.user(user).change(cd).database(db).check(ChangePermission.READ);
+      return true;
+    } catch (AuthException e) {
+      return false;
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/util/IdGenerator.java b/java/com/google/gerrit/server/util/IdGenerator.java
index a9a22d9..276df06 100644
--- a/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/java/com/google/gerrit/server/util/IdGenerator.java
@@ -22,16 +22,6 @@
 /** Simple class to produce 4 billion keys randomly distributed. */
 @Singleton
 public class IdGenerator {
-  /** Format an id created by this class as a hex string. */
-  public static String format(int id) {
-    final char[] r = new char[8];
-    for (int p = 7; 0 <= p; p--) {
-      final int h = id & 0xf;
-      r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10));
-      id >>= 4;
-    }
-    return new String(r);
-  }
 
   private final AtomicInteger gen;
 
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
new file mode 100644
index 0000000..81ca9cd
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -0,0 +1,9 @@
+java_library(
+    name = "git",
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/reviewdb:server",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
similarity index 97%
rename from java/com/google/gerrit/server/util/SubmoduleSectionParser.java
rename to java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
index 6de2fef..a92b7fd 100644
--- a/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
+++ b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2016 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.util;
+package com.google.gerrit.server.util.git;
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
diff --git a/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
index 9f152a5..996ad87 100644
--- a/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
+++ b/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.send.EmailHeader;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
 import java.util.Map;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 6c810a3..4743b35 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index 9fc4cda..2ab13ee 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -86,15 +86,22 @@
     }
     for (ChangeNotes notes : matched) {
       if (!changes.containsKey(notes.getChangeId())
-          && inProject(projectState, notes.getProjectName())
-          && (canMaintainServer
-              || (permissionBackend
-                      .currentUser()
-                      .change(notes)
-                      .database(db)
-                      .test(ChangePermission.READ)
-                  && projectState.statePermitsRead()))) {
-        toAdd.add(notes);
+          && inProject(projectState, notes.getProjectName())) {
+        if (canMaintainServer) {
+          toAdd.add(notes);
+          continue;
+        }
+
+        if (!projectState.statePermitsRead()) {
+          continue;
+        }
+
+        try {
+          permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
+          toAdd.add(notes);
+        } catch (AuthException e) {
+          // Do nothing.
+        }
       }
     }
 
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index b6c2d19..98c649d 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
@@ -277,7 +277,7 @@
   }
 
   private static String id(int id) {
-    return IdGenerator.format(id);
+    return HexFormat.fromInt(id);
   }
 
   void audit(Context ctx, Object result, String cmd) {
@@ -298,7 +298,7 @@
       created = TimeUtil.nowMs();
     } else {
       SshSession session = ctx.getSession();
-      sessionId = IdGenerator.format(session.getSessionId());
+      sessionId = HexFormat.fromInt(session.getSessionId());
       currentUser = session.getUser();
       created = ctx.created;
     }
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index c83660d..8a37cce 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -70,7 +71,9 @@
   @Inject private CreateAccount createAccount;
 
   @Override
-  protected void run() throws OrmException, IOException, ConfigInvalidException, UnloggedFailure {
+  protected void run()
+      throws OrmException, IOException, ConfigInvalidException, UnloggedFailure,
+          PermissionBackendException {
     AccountInput input = new AccountInput();
     input.username = username;
     input.email = email;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 03f9616..917c138 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.AddMembers;
 import com.google.gerrit.server.restapi.group.AddSubgroups;
 import com.google.gerrit.server.restapi.group.CreateGroup;
@@ -100,7 +101,9 @@
   @Inject private AddSubgroups addSubgroups;
 
   @Override
-  protected void run() throws Failure, OrmException, IOException, ConfigInvalidException {
+  protected void run()
+      throws Failure, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     try {
       GroupResource rsrc = createGroup();
 
@@ -117,7 +120,8 @@
   }
 
   private GroupResource createGroup()
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     GroupInput input = new GroupInput();
     input.description = groupDescription;
     input.visibleToAll = visibleToAll;
@@ -132,7 +136,8 @@
   }
 
   private void addMembers(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     AddMembers.Input input =
         AddMembers.Input.fromMembers(
             initialMembers.stream().map(Object::toString).collect(toList()));
@@ -140,7 +145,8 @@
   }
 
   private void addSubgroups(GroupResource rsrc)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     AddSubgroups.Input input =
         AddSubgroups.Input.fromGroups(
             initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
diff --git a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index cfd0a37..1565ecb 100644
--- a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.ListMembers;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -67,7 +68,7 @@
       this.groupCache = groupCache;
     }
 
-    void display(PrintWriter writer) {
+    void display(PrintWriter writer) throws PermissionBackendException {
       Optional<InternalGroup> group = groupCache.get(new AccountGroup.NameKey(name));
       String errorText = "Group not found or not visible\n";
 
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index b30799b..9b517c6 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
@@ -168,7 +168,7 @@
   }
 
   private static String id(SshSession sd) {
-    return sd != null ? IdGenerator.format(sd.getSessionId()) : "";
+    return sd != null ? HexFormat.fromInt(sd.getSessionId()) : "";
   }
 
   private static String time(long now, long time) {
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 43aa978..cf65908 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -19,12 +19,14 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/pgm/init",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
         "//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index 28946dc..e81d0f4 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -22,10 +22,10 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MailHeader;
-import com.google.gerrit.server.mail.send.EmailHeader;
 import com.google.gerrit.server.mail.send.EmailSender;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b3f6222..8f9aa14 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.api.GerritApiModule;
 import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.audit.AuditModule;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -178,6 +179,7 @@
     install(new DefaultPermissionBackendModule());
     install(new SearchingChangeCacheImpl.Module());
     factory(GarbageCollection.Factory.class);
+    install(new AuditModule());
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 0bcf02b..aa9ef80 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -90,6 +90,7 @@
 import com.google.gerrit.gpg.Fingerprint;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.testing.TestKey;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -111,7 +112,6 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index b52efe7..9bc30ba 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -8,4 +8,5 @@
         "noci",
         "no_windows",
     ],
+    deps = ["//java/com/google/gerrit/mail"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index f9284bd..0a42b1e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -126,6 +126,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -140,7 +141,6 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.PostReview;
@@ -449,6 +449,16 @@
   }
 
   @Test
+  public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
+    ConfigInput input = new ConfigInput();
+    input.workInProgressByDefault = InheritableBoolean.TRUE;
+    gApi.projects().name(project.get()).config(input);
+    String changeId =
+        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
+    assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
+  }
+
+  @Test
   public void setReadyForReviewNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result rready = createChange();
     String changeId = rready.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/BUILD b/javatests/com/google/gerrit/acceptance/api/project/BUILD
index 768c20b..97c6f33 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/project/BUILD
@@ -4,4 +4,5 @@
     srcs = glob(["*IT.java"]),
     group = "api_project",
     labels = ["api"],
+    deps = ["//java/com/google/gerrit/index/project"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
new file mode 100644
index 0000000..6fde012
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.RefState;
+import com.google.gerrit.index.project.ProjectField;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.index.project.StalenessChecker;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Test;
+
+public class ProjectIndexerIT extends AbstractDaemonTest {
+  @Inject private ProjectIndexer projectIndexer;
+  @Inject private ProjectIndexCollection indexes;
+  @Inject private IndexConfig indexConfig;
+  @Inject private StalenessChecker stalenessChecker;
+
+  private static final ImmutableSet<String> FIELDS =
+      ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+
+  @Test
+  public void indexProject_indexesRefStateOfProjectAndParents() throws Exception {
+    projectIndexer.index(project);
+    ProjectIndex i = indexes.getSearchIndex();
+    assertThat(i.getSchema().hasField(ProjectField.REF_STATE)).isTrue();
+
+    Optional<FieldBundle> result =
+        i.getRaw(project, QueryOptions.create(indexConfig, 0, 1, FIELDS));
+
+    assertThat(result.isPresent()).isTrue();
+    Iterable<byte[]> refState = result.get().getValue(ProjectField.REF_STATE);
+    assertThat(refState).isNotEmpty();
+
+    Map<Project.NameKey, Collection<RefState>> states = RefState.parseStates(refState).asMap();
+
+    fetch(testRepo, "refs/meta/config:refs/meta/config");
+    Ref projectConfigRef = testRepo.getRepository().exactRef("refs/meta/config");
+    TestRepository<InMemoryRepository> allProjectsRepo = cloneProject(allProjects, admin);
+    fetch(allProjectsRepo, "refs/meta/config:refs/meta/config");
+    Ref allProjectConfigRef = allProjectsRepo.getRepository().exactRef("refs/meta/config");
+    assertThat(states)
+        .containsExactly(
+            project,
+            ImmutableSet.of(RefState.of(projectConfigRef)),
+            allProjects,
+            ImmutableSet.of(RefState.of(allProjectConfigRef)));
+  }
+
+  @Test
+  public void stalenessChecker_currentProject_notStale() throws Exception {
+    assertThat(stalenessChecker.isStale(project)).isFalse();
+  }
+
+  @Test
+  public void stalenessChecker_currentProjectUpdates_isStale() throws Exception {
+    updateProjectConfigWithoutIndexUpdate(project);
+    assertThat(stalenessChecker.isStale(project)).isTrue();
+  }
+
+  @Test
+  public void stalenessChecker_parentProjectUpdates_isStale() throws Exception {
+    updateProjectConfigWithoutIndexUpdate(allProjects);
+    assertThat(stalenessChecker.isStale(project)).isTrue();
+  }
+
+  @Test
+  public void stalenessChecker_hierarchyChange_isStale() throws Exception {
+    Project.NameKey p1 = createProject("p1", allProjects);
+    Project.NameKey p2 = createProject("p2", allProjects);
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().getProject().setParentName(p1);
+      u.save();
+    }
+    assertThat(stalenessChecker.isStale(project)).isFalse();
+
+    updateProjectConfigWithoutIndexUpdate(p1, c -> c.getProject().setParentName(p2));
+    assertThat(stalenessChecker.isStale(project)).isTrue();
+  }
+
+  private void updateProjectConfigWithoutIndexUpdate(Project.NameKey project) throws Exception {
+    updateProjectConfigWithoutIndexUpdate(
+        project, c -> c.getProject().setDescription("making it stale"));
+  }
+
+  private void updateProjectConfigWithoutIndexUpdate(
+      Project.NameKey project, Consumer<ProjectConfig> update) throws Exception {
+    try (AutoCloseable ignored = disableProjectIndex()) {
+      try (ProjectConfigUpdate u = updateProject(project)) {
+        update.accept(u.getConfig());
+        u.save();
+      }
+    } catch (UnsupportedOperationException e) {
+      // Drop, as we just wanted to drop the index update
+      return;
+    }
+    fail("should have a UnsupportedOperationException");
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 51946e9..53cc5ad 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.changes.FileApi;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -112,16 +113,115 @@
   }
 
   @Test
-  public void diffDeletedFile() throws Exception {
+  public void deletedFileIsIncludedInDiff() throws Exception {
     gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
     gApi.changes().id(changeId).edit().publish();
 
     Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
     assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
+  }
 
-    DiffInfo diff = getDiffRequest(changeId, CURRENT, FILE_NAME).get();
-    assertThat(diff.metaA.lines).isEqualTo(100);
-    assertThat(diff.metaB).isNull();
+  @Test
+  public void numberOfLinesInDiffOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+    assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+  }
+
+  @Test
+  public void numberOfLinesInDiffOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+  }
+
+  @Test
+  public void deletedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfA()
+        .containsExactly("Line 1", "Line 2", "Line 3");
+  }
+
+  @Test
+  public void deletedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().deleteFile(filePath);
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfA()
+        .containsExactly("Line 1", "Line 2", "Line 3", "");
   }
 
   @Test
@@ -136,6 +236,91 @@
   }
 
   @Test
+  public void numberOfLinesInDiffOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).metaA().isNull();
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+    assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void numberOfLinesInDiffOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).metaA().isNull();
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void numberOfLinesInFileInfoOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+    assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void addedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfB()
+        .containsExactly("Line 1", "Line 2", "Line 3");
+  }
+
+  @Test
+  public void addedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .linesOfB()
+        .containsExactly("Line 1", "Line 2", "Line 3", "");
+  }
+
+  @Test
   public void renamedFileIsIncludedInDiff() throws Exception {
     String newFilePath = "a_new_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
@@ -193,11 +378,11 @@
 
     // automerge
     diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "foo").get();
-    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaA.lines).isEqualTo(6);
     assertThat(diff.metaB.lines).isEqualTo(1);
 
     diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "bar").get();
-    assertThat(diff.metaA.lines).isEqualTo(5);
+    assertThat(diff.metaA.lines).isEqualTo(6);
     assertThat(diff.metaB.lines).isEqualTo(1);
 
     // parent 1
@@ -212,6 +397,596 @@
   }
 
   @Test
+  public void diffOfUnmodifiedFileMarksAllLinesAsCommon() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo)
+        .content()
+        .onlyElement()
+        .commonLines()
+        .containsAllOf("Line 1", "Line 2", "Line 3")
+        .inOrder();
+    assertThat(diffInfo).content().onlyElement().linesOfA().isNull();
+    assertThat(diffInfo).content().onlyElement().linesOfB().isNull();
+  }
+
+  @Test
+  public void diffOfUnmodifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfUnmodifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+    gApi.changes().id(changeId).edit().publish();
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("Line 3");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void diffOfModifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfModifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("Line 3");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void diffOfModifiedLastLineWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3\n";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3\n", "Line three\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+  }
+
+  @Test
+  public void diffOfModifiedLastLineWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+    String filePath = "a_new_file.txt";
+    String fileContent = "Line 1\nLine 2\nLine 3";
+    gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+    gApi.changes().id(changeId).edit().publish();
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3", "Line three"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().lastElement().linesOfA().containsExactly("Line 3");
+    assertThat(diffInfo).content().lastElement().linesOfB().containsExactly("Line three");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().isNull();
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "Line 102");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .withBase(previousPatchSetId)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .linesOfB()
+        .containsExactly("Line 101", "Line 102", "");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(103);
+  }
+
+  @Test
+  public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().isNull();
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeAndAfterwardsMeansOneInsertedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(101);
+  }
+
+  @Test
+  public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansOneInsertedLine()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+  }
+
+  @Test
+  public void hunkForModifiedLastLineIsCombinedWithHunkForAddedNewlineAtEnd() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one oh one", "");
+  }
+
+  @Test
+  public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoAddedAtEnd()
+      throws Exception {
+    assume().that(intraline).isTrue();
+
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfA()
+        .containsExactly(ImmutableList.of(5, 3));
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfB()
+        .containsExactly(ImmutableList.of(5, 11));
+  }
+
+  @Test
+  public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForAddedNewlineAtEnd()
+      throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n").concat("\n"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().isNull();
+    assertThat(diffInfo).content().element(3).linesOfB().containsExactly("");
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 100");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(1).linesOfB().isNull();
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(previousPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansOneDeletedLine()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+    String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(previousPatchSetId);
+    // Inherited from Git: An empty last line is ignored in the count.
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+    assertThat(diffInfo).content().element(1).linesOfB().isNull();
+    assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeAndAfterwardsMeansOneDeletedLine() throws Exception {
+    addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME)
+            .withBase(initialPatchSetId)
+            .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+            .get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+    assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+    assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+  }
+
+  @Test
+  public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansTwoModifiedLines()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(changeId).current().files(initialPatchSetId);
+    assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+    assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+  }
+
+  @Test
+  public void hunkForModifiedLastLineIsCombinedWithHunkForDeletedNewlineAtEnd() throws Exception {
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+  }
+
+  @Test
+  public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoDeletedAtEnd()
+      throws Exception {
+    assume().that(intraline).isTrue();
+
+    addModifiedPatchSet(
+        changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfA()
+        .containsExactly(ImmutableList.of(5, 4));
+    assertThat(diffInfo)
+        .content()
+        .element(1)
+        .intralineEditsOfB()
+        .containsExactly(ImmutableList.of(5, 11));
+  }
+
+  @Test
+  public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForDeletedNewlineAtEnd()
+      throws Exception {
+    addModifiedPatchSet(
+        changeId,
+        FILE_NAME,
+        fileContent ->
+            fileContent
+                .replace("Line 99\n", "Line ninety-nine\n")
+                .replace("Line 100\n", "Line 100"));
+
+    DiffInfo diffInfo =
+        getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+    assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99");
+    assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line ninety-nine");
+    assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+    assertThat(diffInfo).content().element(3).linesOfA().containsExactly("");
+    assertThat(diffInfo).content().element(3).linesOfB().isNull();
+  }
+
+  @Test
   public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
     ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content");
 
@@ -382,7 +1157,7 @@
         .content()
         .element(1)
         .commonLines()
-        .containsExactly("Line 2", "Line 3", "Line 4", "Line 5")
+        .containsExactly("Line 2", "Line 3", "Line 4", "Line 5", "")
         .inOrder();
   }
 
@@ -430,7 +1205,7 @@
         .content()
         .element(2)
         .commonLines()
-        .containsExactly("Line 4", "Line 5", "Line 6")
+        .containsExactly("Line 4", "Line 5", "Line 6", "")
         .inOrder();
   }
 
@@ -1027,7 +1802,7 @@
     DiffInfo diffInfo =
         getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
     assertThat(diffInfo).changeType().isEqualTo(ChangeType.DELETED);
-    assertThat(diffInfo).content().element(0).linesOfA().hasSize(100);
+    assertThat(diffInfo).content().element(0).linesOfA().hasSize(101);
     assertThat(diffInfo).content().element(0).linesOfB().isNull();
     assertThat(diffInfo).content().element(0).isNotDueToRebase();
 
@@ -1050,7 +1825,7 @@
         getDiffRequest(changeId, CURRENT, newFilePath).withBase(initialPatchSetId).get();
     assertThat(diffInfo).changeType().isEqualTo(ChangeType.ADDED);
     assertThat(diffInfo).content().element(0).linesOfA().isNull();
-    assertThat(diffInfo).content().element(0).linesOfB().hasSize(3);
+    assertThat(diffInfo).content().element(0).linesOfB().hasSize(4);
     assertThat(diffInfo).content().element(0).isNotDueToRebase();
 
     Map<String, FileInfo> changedFiles =
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index cfa7ec4..b4ae8a2 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -71,6 +71,7 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.testing.EditInfoSubject;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
@@ -84,7 +85,6 @@
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
diff --git a/javatests/com/google/gerrit/acceptance/git/BUILD b/javatests/com/google/gerrit/acceptance/git/BUILD
index 7a6a3c4..3541fec 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -16,6 +16,7 @@
     srcs = ["AbstractPushForReview.java"],
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
+        "//java/com/google/gerrit/mail",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index 583ecc8..ea4c87d 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -22,6 +22,7 @@
     group = "elastic",
     labels = [
         "elastic",
+        "exclusive",
         "flaky",
         "pgm",
         "no_windows",
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/BUILD b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
index 6a4b4a7..46eb0ba 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
@@ -15,6 +15,7 @@
     labels = ["rest"],
     deps = [
         ":submit_util",
+        "//java/com/google/gerrit/mail",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 84c9c03..06b67d8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -32,7 +32,7 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import java.util.List;
 import org.junit.Before;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 129d98a..2259999 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -46,8 +46,8 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.restapi.change.PostReviewers;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.stream.JsonReader;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
new file mode 100644
index 0000000..34d87d0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -0,0 +1,158 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
+  private Project.NameKey project1;
+  private Project.NameKey project2;
+
+  @Before
+  public void setUp() throws Exception {
+    project1 = createProject("project-1");
+    project2 = createProject("project-2", project1);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    setApiUser(admin);
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    prefs.workInProgressByDefault = false;
+    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+  }
+
+  @Test
+  public void createChangeWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+    assertThat(info.workInProgress).isNull();
+  }
+
+  @Test
+  public void createChangeWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
+    setWorkInProgressByDefaultForProject(project2);
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
+  }
+
+  @Test
+  public void createChangeWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    setWorkInProgressByDefaultForUser();
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
+  }
+
+  @Test
+  public void createChangeBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
+    setWorkInProgressByDefaultForProject(project2);
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    input.workInProgress = false;
+    assertThat(gApi.changes().create(input).get().workInProgress).isNull();
+  }
+
+  @Test
+  public void createChangeBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    setWorkInProgressByDefaultForUser();
+    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    input.workInProgress = false;
+    assertThat(gApi.changes().create(input).get().workInProgress).isNull();
+  }
+
+  @Test
+  public void createChangeWithWorkInProgressByDefaultForProjectInherited() throws Exception {
+    setWorkInProgressByDefaultForProject(project1);
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+    assertThat(info.workInProgress).isTrue();
+  }
+
+  @Test
+  public void pushWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
+    setWorkInProgressByDefaultForProject(project2);
+    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+  }
+
+  @Test
+  public void pushWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    setWorkInProgressByDefaultForUser();
+    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+  }
+
+  @Test
+  public void pushBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
+    setWorkInProgressByDefaultForProject(project2);
+    assertThat(
+            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+        .isFalse();
+  }
+
+  @Test
+  public void pushBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    setWorkInProgressByDefaultForUser();
+    assertThat(
+            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+        .isFalse();
+  }
+
+  @Test
+  public void pushWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isFalse();
+  }
+
+  @Test
+  public void pushWorkInProgressByDefaultForProjectInherited() throws Exception {
+    setWorkInProgressByDefaultForProject(project1);
+    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+  }
+
+  private void setWorkInProgressByDefaultForProject(Project.NameKey p) throws Exception {
+    ConfigInput input = new ConfigInput();
+    input.workInProgressByDefault = InheritableBoolean.TRUE;
+    gApi.projects().name(p.get()).config(input);
+  }
+
+  private void setWorkInProgressByDefaultForUser() throws Exception {
+    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+    prefs.workInProgressByDefault = true;
+    gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+  }
+
+  private PushOneCommit.Result createChange(Project.NameKey p) throws Exception {
+    return createChange(p, "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(Project.NameKey p, String r) throws Exception {
+    TestRepository<InMemoryRepository> testRepo = cloneProject(p);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to(r);
+    result.assertOkStatus();
+    return result;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index 32f1ce5..c1b5cf4 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
 import java.time.Instant;
 import java.util.HashMap;
 import org.junit.Ignore;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/BUILD b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
index 4175272..b5ad425 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/mail/BUILD
@@ -3,6 +3,7 @@
 DEPS = [
     "//lib/greenmail",
     "//lib/mail",
+    "//java/com/google/gerrit/mail",
 ]
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 7096581..9645a94 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -42,10 +42,14 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.PostReview;
@@ -970,6 +974,38 @@
   }
 
   @Test
+  public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
+    setWorkInProgressByDefault(project, InheritableBoolean.TRUE);
+    StagedPreChange spc = stagePreChange("refs/for/master");
+    Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
+    assertThat(sender).notSent();
+  }
+
+  @Test
+  public void createWipChangeWithWorkInProgressByDefaultForUser() throws Exception {
+    // Make sure owner user is created
+    StagedChange sc = stageReviewableChange();
+    // All was cleaned already
+    assertThat(sender).notSent();
+
+    // Toggle workInProgress flag for owner
+    GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    prefs.workInProgressByDefault = true;
+    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+
+    // Create another change without notification that should be wip
+    StagedPreChange spc = stagePreChange("refs/for/master");
+    Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
+    assertThat(sender).notSent();
+
+    // Clean up workInProgressByDefault by owner
+    prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+    Truth.assertThat(prefs.workInProgressByDefault).isTrue();
+    prefs.workInProgressByDefault = false;
+    gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+  }
+
+  @Test
   public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception {
     stagePreChange("refs/for/master%notify=OWNER_REVIEWERS");
     assertThat(sender).notSent();
@@ -2646,4 +2682,11 @@
     // PolyGerrit current immediately follows up with a review.
     gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
   }
+
+  private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
+      throws Exception {
+    ConfigInput input = new ConfigInput();
+    input.workInProgressByDefault = v;
+    gApi.projects().name(p.get()).config(input);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index 438954c..13f0416 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -21,8 +21,8 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.server.mail.MailUtil;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.mail.receive.MailProcessor;
 import com.google.inject.Inject;
 import java.time.ZoneId;
@@ -100,7 +100,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 3315a33..988b051 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.server.mail.MailUtil;
-import com.google.gerrit.server.mail.send.EmailHeader;
+import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.sql.Timestamp;
@@ -147,7 +147,7 @@
             .contains(
                 entry.getKey()
                     + ": "
-                    + MailUtil.rfcDateformatter.format(
+                    + MailProcessingUtil.rfcDateformatter.format(
                         ZonedDateTime.ofInstant(
                             ((Timestamp) entry.getValue()).toInstant(), ZoneId.of("UTC"))));
       } else {
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index f34fe33..b9f30d6 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.server.mail.MailUtil;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.mail.receive.MailProcessor;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
@@ -40,7 +40,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
@@ -68,7 +68,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
@@ -104,7 +104,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
@@ -141,7 +141,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
@@ -171,7 +171,7 @@
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
     assertThat(comments).hasSize(2);
 
@@ -206,7 +206,7 @@
     List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
     assertThat(comments).hasSize(2);
     String ts =
-        MailUtil.rfcDateformatter.format(
+        MailProcessingUtil.rfcDateformatter.format(
             ZonedDateTime.ofInstant(comments.get(0).updated.toInstant(), ZoneId.of("UTC")));
 
     // Build Message
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 4f51e1f..8d21b5b 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.server.mail.send.EmailHeader;
+import com.google.gerrit.mail.EmailHeader;
 import java.net.URI;
 import java.util.Map;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/server/project/BUILD b/javatests/com/google/gerrit/acceptance/server/project/BUILD
index efa1cdb..42dfbac 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/project/BUILD
@@ -4,4 +4,5 @@
     srcs = glob(["*IT.java"]),
     group = "server_project",
     labels = ["server"],
+    deps = ["//java/com/google/gerrit/mail"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index ca0cae4..cfdd781 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -26,11 +26,11 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.git.NotifyConfig;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import java.util.EnumSet;
 import java.util.List;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index eefd9d3..b195ecc 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -25,8 +25,9 @@
     srcs = ["ElasticIndexIT.java"],
     group = "elastic",
     labels = [
-        "elastic",
         "docker",
+        "elastic",
+        "exclusive",
         "ssh",
     ],
     deps = [
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 13bb2f2..95da5a6 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -24,16 +24,14 @@
 import org.eclipse.jgit.lib.Config;
 
 public class ElasticIndexIT extends AbstractIndexTests {
-  private static ElasticContainer<?> container;
 
   private static Config getConfig(ElasticVersion version) {
     ElasticNodeInfo elasticNodeInfo;
-    container = ElasticContainer.createAndStart(version);
+    ElasticContainer<?> container = ElasticContainer.createAndStart(version);
     elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
     String indicesPrefix = UUID.randomUUID().toString();
     Config cfg = new Config();
-    String password = version == ElasticVersion.V5_6 ? "changeme" : null;
-    ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, password);
+    ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
     return cfg;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/tests.bzl b/javatests/com/google/gerrit/acceptance/tests.bzl
index 4b3b802d..08556a0 100644
--- a/javatests/com/google/gerrit/acceptance/tests.bzl
+++ b/javatests/com/google/gerrit/acceptance/tests.bzl
@@ -1,21 +1,21 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
 def acceptance_tests(
-    group,
-    deps = [],
-    labels = [],
-    vm_args = ['-Xmx256m'],
-    **kwargs):
-  junit_tests(
-    name = group,
-    deps = deps + [
-      '//java/com/google/gerrit/acceptance:lib',
-    ],
-    tags = labels + [
-      'acceptance',
-      'slow',
-    ],
-    size = "large",
-    jvm_flags = vm_args,
-    **kwargs
-  )
+        group,
+        deps = [],
+        labels = [],
+        vm_args = ["-Xmx256m"],
+        **kwargs):
+    junit_tests(
+        name = group,
+        deps = deps + [
+            "//java/com/google/gerrit/acceptance:lib",
+        ],
+        tags = labels + [
+            "acceptance",
+            "slow",
+        ],
+        size = "large",
+        jvm_flags = vm_args,
+        **kwargs
+    )
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index ba9a5bc..c4f2c0a 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -1,32 +1,14 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
-SERVER_TEST_SRCS = [
-    "AutoValueTest.java",
-    "VersionTest.java",
-]
-
 junit_tests(
-    name = "client_tests",
-    srcs = glob(
-        ["**/*.java"],
-        exclude = SERVER_TEST_SRCS,
-    ),
-    deps = [
-        "//java/com/google/gerrit/common:client",
-        "//lib:guava",
-        "//lib:junit",
-        "//lib/truth",
-    ],
-)
-
-junit_tests(
-    name = "server_tests",
-    srcs = SERVER_TEST_SRCS,
+    name = "common_tests",
+    srcs = glob(["**/*.java"]),
     tags = ["no_windows"],
     deps = [
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/common:version",
         "//java/com/google/gerrit/launcher",
+        "//java/com/google/gerrit/reviewdb:server",
         "//lib:guava",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
new file mode 100644
index 0000000..717e122
--- /dev/null
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -0,0 +1,157 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class GroupReferenceTest {
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void forGroupDescription() {
+    String name = "foo";
+    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    GroupReference groupReference =
+        GroupReference.forGroup(
+            new GroupDescription.Basic() {
+
+              @Override
+              public String getUrl() {
+                return null;
+              }
+
+              @Override
+              public String getName() {
+                return name;
+              }
+
+              @Override
+              public UUID getGroupUUID() {
+                return uuid;
+              }
+
+              @Override
+              public String getEmailAddress() {
+                return null;
+              }
+            });
+    assertThat(groupReference.getName()).isEqualTo(name);
+    assertThat(groupReference.getUUID()).isEqualTo(uuid);
+  }
+
+  @Test
+  public void create() {
+    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid");
+    String name = "foo";
+    GroupReference groupReference = new GroupReference(uuid, name);
+    assertThat(groupReference.getUUID()).isEqualTo(uuid);
+    assertThat(groupReference.getName()).isEqualTo(name);
+  }
+
+  @Test
+  public void createWithoutUuid() {
+    // GroupReferences where the UUID is null are used to represent groups from project.config that
+    // cannot be resolved.
+    String name = "foo";
+    GroupReference groupReference = new GroupReference(null, name);
+    assertThat(groupReference.getUUID()).isNull();
+    assertThat(groupReference.getName()).isEqualTo(name);
+  }
+
+  @Test
+  public void cannotCreateWithoutName() {
+    exception.expect(NullPointerException.class);
+    new GroupReference(new AccountGroup.UUID("uuid"), null);
+  }
+
+  @Test
+  public void isGroupReference() {
+    assertThat(GroupReference.isGroupReference("foo")).isFalse();
+    assertThat(GroupReference.isGroupReference("groupfoo")).isFalse();
+    assertThat(GroupReference.isGroupReference("group foo")).isTrue();
+    assertThat(GroupReference.isGroupReference("group foo-bar")).isTrue();
+    assertThat(GroupReference.isGroupReference("group foo bar")).isTrue();
+  }
+
+  @Test
+  public void extractGroupName() {
+    assertThat(GroupReference.extractGroupName("foo")).isNull();
+    assertThat(GroupReference.extractGroupName("groupfoo")).isNull();
+    assertThat(GroupReference.extractGroupName("group foo")).isEqualTo("foo");
+    assertThat(GroupReference.extractGroupName("group foo-bar")).isEqualTo("foo-bar");
+    assertThat(GroupReference.extractGroupName("group foo bar")).isEqualTo("foo bar");
+  }
+
+  @Test
+  public void getAndSetUuid() {
+    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    String name = "foo";
+    GroupReference groupReference = new GroupReference(uuid, name);
+    assertThat(groupReference.getUUID()).isEqualTo(uuid);
+
+    AccountGroup.UUID uuid2 = new AccountGroup.UUID("uuid-bar");
+    groupReference.setUUID(uuid2);
+    assertThat(groupReference.getUUID()).isEqualTo(uuid2);
+
+    // GroupReferences where the UUID is null are used to represent groups from project.config that
+    // cannot be resolved.
+    groupReference.setUUID(null);
+    assertThat(groupReference.getUUID()).isNull();
+  }
+
+  @Test
+  public void getAndSetName() {
+    AccountGroup.UUID uuid = new AccountGroup.UUID("uuid-foo");
+    String name = "foo";
+    GroupReference groupReference = new GroupReference(uuid, name);
+    assertThat(groupReference.getName()).isEqualTo(name);
+
+    String name2 = "bar";
+    groupReference.setName(name2);
+    assertThat(groupReference.getName()).isEqualTo(name2);
+
+    exception.expect(NullPointerException.class);
+    groupReference.setName(null);
+  }
+
+  @Test
+  public void toConfigValue() {
+    String name = "foo";
+    GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-foo"), name);
+    assertThat(groupReference.toConfigValue()).isEqualTo("group " + name);
+  }
+
+  @Test
+  public void testEquals() {
+    AccountGroup.UUID uuid1 = new AccountGroup.UUID("uuid-foo");
+    AccountGroup.UUID uuid2 = new AccountGroup.UUID("uuid-bar");
+    String name1 = "foo";
+    String name2 = "bar";
+
+    GroupReference groupReference1 = new GroupReference(uuid1, name1);
+    GroupReference groupReference2 = new GroupReference(uuid1, name2);
+    GroupReference groupReference3 = new GroupReference(uuid2, name1);
+
+    assertThat(groupReference1.equals(groupReference2)).isTrue();
+    assertThat(groupReference1.equals(groupReference3)).isFalse();
+    assertThat(groupReference2.equals(groupReference3)).isFalse();
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 595c887..cc849eb 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -49,10 +49,11 @@
 ELASTICSEARCH_TAGS = [
     "docker",
     "elastic",
+    "exclusive",
 ]
 
 [junit_tests(
-    name = "elasticsearch_%ss_test" % name,
+    name = "elasticsearch_query_%ss_test" % name,
     size = "large",
     srcs = [src],
     tags = ELASTICSEARCH_TAGS,
@@ -60,7 +61,7 @@
 ) for name, src in ELASTICSEARCH_TESTS.items()]
 
 [junit_tests(
-    name = "elasticsearch_%ss_test_V5" % name,
+    name = "elasticsearch_query_%ss_test_V5" % name,
     size = "large",
     srcs = [src],
     tags = ELASTICSEARCH_TAGS,
@@ -68,14 +69,14 @@
 ) for name, src in ELASTICSEARCH_TESTS_V5.items()]
 
 [junit_tests(
-    name = "elasticsearch_%ss_test_V6" % name,
+    name = "elasticsearch_query_%ss_test_V6" % name,
     size = "large",
     srcs = [src],
     tags = ELASTICSEARCH_TAGS + ["flaky"],
     deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
 ) for name, src in ELASTICSEARCH_TESTS_V6.items()]
 
-[junit_tests(
+junit_tests(
     name = "elasticsearch_tests",
     size = "small",
     srcs = glob(
@@ -90,4 +91,4 @@
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
-)]
+)
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 02e0ba2..b46e040 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -32,11 +32,12 @@
     }
   }
 
-  public static void configure(Config config, int port, String prefix, String password) {
+  public static void configure(Config config, int port, String prefix, ElasticVersion version) {
     config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
     config.setString("elasticsearch", null, "server", "http://localhost:" + port);
     config.setString("elasticsearch", null, "prefix", prefix);
     config.setInt("index", null, "maxLimit", 10000);
+    String password = version == ElasticVersion.V5_6 ? "changeme" : null;
     if (password != null) {
       config.setString("elasticsearch", null, "password", password);
     }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index 0fcdb20..5d2f944 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -67,7 +67,8 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+    ElasticTestUtils.configure(
+        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index 4520020..5d76162 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -67,7 +67,8 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+    ElasticTestUtils.configure(
+        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index d953139..9ce2e93 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -67,7 +67,8 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+    ElasticTestUtils.configure(
+        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 7b49e1d..4184935 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -67,7 +67,8 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(
+        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/server/mail/receive/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/mail/receive/AbstractParserTest.java
rename to javatests/com/google/gerrit/mail/AbstractParserTest.java
index 0e894a6..5219ce8 100644
--- a/javatests/com/google/gerrit/server/mail/receive/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.server.mail.Address;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
diff --git a/javatests/com/google/gerrit/server/mail/AddressTest.java b/javatests/com/google/gerrit/mail/AddressTest.java
similarity index 81%
rename from javatests/com/google/gerrit/server/mail/AddressTest.java
rename to javatests/com/google/gerrit/mail/AddressTest.java
index 7dbd563..53ff1fe 100644
--- a/javatests/com/google/gerrit/server/mail/AddressTest.java
+++ b/javatests/com/google/gerrit/mail/AddressTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
@@ -24,57 +24,57 @@
   @Test
   public void parse_NameEmail1() {
     final Address a = Address.parse("A U Thor <author@example.com>");
-    assertThat(a.name).isEqualTo("A U Thor");
-    assertThat(a.email).isEqualTo("author@example.com");
+    assertThat(a.getName()).isEqualTo("A U Thor");
+    assertThat(a.getEmail()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_NameEmail2() {
     final Address a = Address.parse("A <a@b>");
-    assertThat(a.name).isEqualTo("A");
-    assertThat(a.email).isEqualTo("a@b");
+    assertThat(a.getName()).isEqualTo("A");
+    assertThat(a.getEmail()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NameEmail3() {
     final Address a = Address.parse("<a@b>");
-    assertThat(a.name).isNull();
-    assertThat(a.email).isEqualTo("a@b");
+    assertThat(a.getName()).isNull();
+    assertThat(a.getEmail()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NameEmail4() {
     final Address a = Address.parse("A U Thor<author@example.com>");
-    assertThat(a.name).isEqualTo("A U Thor");
-    assertThat(a.email).isEqualTo("author@example.com");
+    assertThat(a.getName()).isEqualTo("A U Thor");
+    assertThat(a.getEmail()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_NameEmail5() {
     final Address a = Address.parse("A U Thor  <author@example.com>");
-    assertThat(a.name).isEqualTo("A U Thor");
-    assertThat(a.email).isEqualTo("author@example.com");
+    assertThat(a.getName()).isEqualTo("A U Thor");
+    assertThat(a.getEmail()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_Email1() {
     final Address a = Address.parse("author@example.com");
-    assertThat(a.name).isNull();
-    assertThat(a.email).isEqualTo("author@example.com");
+    assertThat(a.getName()).isNull();
+    assertThat(a.getEmail()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_Email2() {
     final Address a = Address.parse("a@b");
-    assertThat(a.name).isNull();
-    assertThat(a.email).isEqualTo("a@b");
+    assertThat(a.getName()).isNull();
+    assertThat(a.getEmail()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NewTLD() {
     Address a = Address.parse("A U Thor <author@example.systems>");
-    assertThat(a.name).isEqualTo("A U Thor");
-    assertThat(a.email).isEqualTo("author@example.systems");
+    assertThat(a.getName()).isEqualTo("A U Thor");
+    assertThat(a.getEmail()).isEqualTo("author@example.systems");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD
new file mode 100644
index 0000000..488bbcc
--- /dev/null
+++ b/javatests/com/google/gerrit/mail/BUILD
@@ -0,0 +1,36 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "maillib_tests",
+    size = "small",
+    srcs = glob(
+        ["**/*.java"],
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+        "//java/com/google/gerrit/index",
+        "//java/com/google/gerrit/index:query_exception",
+        "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/mail",
+        "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/project/testing:project-test-util",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/org/eclipse/jgit:server",
+        "//lib:grappa",
+        "//lib:gson",
+        "//lib:guava-retrying",
+        "//lib:gwtorm",
+        "//lib/commons:codec",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/truth",
+        "//lib/truth:truth-java8-extension",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java b/javatests/com/google/gerrit/mail/GenericHtmlParserTest.java
similarity index 98%
rename from javatests/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java
rename to javatests/com/google/gerrit/mail/GenericHtmlParserTest.java
index f78953d..4718bad 100644
--- a/javatests/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java
+++ b/javatests/com/google/gerrit/mail/GenericHtmlParserTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 /** Test parser for a generic Html email client response */
 public class GenericHtmlParserTest extends HtmlParserTest {
diff --git a/javatests/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java b/javatests/com/google/gerrit/mail/GmailHtmlParserTest.java
similarity index 98%
rename from javatests/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
rename to javatests/com/google/gerrit/mail/GmailHtmlParserTest.java
index df71629..f597dee 100644
--- a/javatests/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
+++ b/javatests/com/google/gerrit/mail/GmailHtmlParserTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 public class GmailHtmlParserTest extends HtmlParserTest {
   @Override
diff --git a/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java b/javatests/com/google/gerrit/mail/HtmlParserTest.java
similarity index 98%
rename from javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java
rename to javatests/com/google/gerrit/mail/HtmlParserTest.java
index d88e09f..d630bd6 100644
--- a/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java
+++ b/javatests/com/google/gerrit/mail/HtmlParserTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
similarity index 96%
rename from javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
rename to javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index 071dc4b..2d2c2ea 100644
--- a/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -12,12 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MailHeader;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/server/mail/receive/ParserUtilTest.java b/javatests/com/google/gerrit/mail/ParserUtilTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/mail/receive/ParserUtilTest.java
rename to javatests/com/google/gerrit/mail/ParserUtilTest.java
index dfa492c..47a5367 100644
--- a/javatests/com/google/gerrit/server/mail/receive/ParserUtilTest.java
+++ b/javatests/com/google/gerrit/mail/ParserUtilTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/javatests/com/google/gerrit/server/mail/receive/RawMailParserTest.java b/javatests/com/google/gerrit/mail/RawMailParserTest.java
similarity index 83%
rename from javatests/com/google/gerrit/server/mail/receive/RawMailParserTest.java
rename to javatests/com/google/gerrit/mail/RawMailParserTest.java
index fb52947..9049704 100644
--- a/javatests/com/google/gerrit/server/mail/receive/RawMailParserTest.java
+++ b/javatests/com/google/gerrit/mail/RawMailParserTest.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.mail.receive.data.AttachmentMessage;
-import com.google.gerrit.server.mail.receive.data.Base64HeaderMessage;
-import com.google.gerrit.server.mail.receive.data.HtmlMimeMessage;
-import com.google.gerrit.server.mail.receive.data.NonUTF8Message;
-import com.google.gerrit.server.mail.receive.data.QuotedPrintableHeaderMessage;
-import com.google.gerrit.server.mail.receive.data.RawMailMessage;
-import com.google.gerrit.server.mail.receive.data.SimpleTextMessage;
+import com.google.gerrit.mail.data.AttachmentMessage;
+import com.google.gerrit.mail.data.Base64HeaderMessage;
+import com.google.gerrit.mail.data.HtmlMimeMessage;
+import com.google.gerrit.mail.data.NonUTF8Message;
+import com.google.gerrit.mail.data.QuotedPrintableHeaderMessage;
+import com.google.gerrit.mail.data.RawMailMessage;
+import com.google.gerrit.mail.data.SimpleTextMessage;
 import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
 
diff --git a/javatests/com/google/gerrit/server/mail/receive/TextParserTest.java b/javatests/com/google/gerrit/mail/TextParserTest.java
similarity index 99%
rename from javatests/com/google/gerrit/server/mail/receive/TextParserTest.java
rename to javatests/com/google/gerrit/mail/TextParserTest.java
index 89e1f22..e11321a 100644
--- a/javatests/com/google/gerrit/server/mail/receive/TextParserTest.java
+++ b/javatests/com/google/gerrit/mail/TextParserTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive;
+package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
similarity index 95%
rename from javatests/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
rename to javatests/com/google/gerrit/mail/data/AttachmentMessage.java
index eb4d180..1d94d68 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
+++ b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
similarity index 93%
rename from javatests/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
rename to javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
index 91dc6f1..aa19537 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
similarity index 96%
rename from javatests/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
rename to javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
index 756581f..1d68cc8 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
+++ b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
similarity index 93%
rename from javatests/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
rename to javatests/com/google/gerrit/mail/data/NonUTF8Message.java
index 3fafd4b..32915e7 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java
+++ b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
@@ -11,10 +11,10 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
similarity index 93%
rename from javatests/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
rename to javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
index 2dc48b5..47e813a 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/RawMailMessage.java b/javatests/com/google/gerrit/mail/data/RawMailMessage.java
similarity index 89%
rename from javatests/com/google/gerrit/server/mail/receive/data/RawMailMessage.java
rename to javatests/com/google/gerrit/mail/data/RawMailMessage.java
index 2af82ad..53c782c 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/RawMailMessage.java
+++ b/javatests/com/google/gerrit/mail/data/RawMailMessage.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.MailMessage;
 import org.junit.Ignore;
 
 /** Base class for all email parsing tests. */
diff --git a/javatests/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
similarity index 97%
rename from javatests/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
rename to javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
index aa5b78a..a8f5b94 100644
--- a/javatests/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
+++ b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail.receive.data;
+package com.google.gerrit.mail.data;
 
-import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index f405c28..569b0b9 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -41,6 +41,7 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index acb33e9..51bda66 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -24,11 +24,11 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.index.RefState;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.RefState;
 import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.testing.GerritBaseTests;
diff --git a/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
new file mode 100644
index 0000000..9bb6951
--- /dev/null
+++ b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ioutil;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class HexFormatTest {
+
+  @Test
+  public void fromInt() {
+    assertEquals("0000000f", HexFormat.fromInt(0xf));
+    assertEquals("801234ab", HexFormat.fromInt(0x801234ab));
+    assertEquals("deadbeef", HexFormat.fromInt(0xdeadbeef));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
index a7234f4..f8a613a 100644
--- a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -16,7 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.testing.GerritBaseTests;
 import java.time.Instant;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 7028bb2..f99c356 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -22,12 +22,12 @@
 import static org.easymock.EasyMock.verify;
 
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.mail.Address;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 3d65eae..574f6ac 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -30,6 +30,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -47,7 +48,6 @@
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
 import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
 import com.google.gwtorm.client.KeyUtil;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 3a98127..852c8bc 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -35,6 +35,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -50,7 +51,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.config.GerritServerId;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.testing.TestChanges;
diff --git a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
index a5c4e49..2e19d20 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
@@ -33,15 +33,15 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.notedb.CommentJsonMigrator.ProjectMigrationResult;
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -57,13 +57,14 @@
   @Inject private ChangeNoteUtil noteUtil;
   @Inject private CommentsUtil commentsUtil;
   @Inject private LegacyChangeNoteWrite legacyChangeNoteWrite;
+  @Inject private AllUsersName allUsersName;
 
   private AtomicInteger uuidCounter;
 
   @Before
   public void setUpCounter() {
     uuidCounter = new AtomicInteger();
-    migrator = new CommentJsonMigrator(new ChangeNoteJson(), "gerrit");
+    migrator = new CommentJsonMigrator(new ChangeNoteJson(), "gerrit", allUsersName);
   }
 
   @Test
@@ -90,7 +91,7 @@
             getRevId(notes, 2), ps2Comment.toString());
 
     ChangeNotes oldNotes = notes;
-    migrate(project, migrator::migrateChanges, 0);
+    checkMigrate(project, ImmutableList.of());
     assertNoDifferences(notes, oldNotes);
     assertThat(notes.getMetaId()).isEqualTo(oldNotes.getMetaId());
   }
@@ -163,7 +164,7 @@
         .containsExactly(ps1Comment1.key, true, ps1Comment2.key, true, ps2Comment1.key, true);
 
     ChangeNotes oldNotes = notes;
-    migrate(project, migrator::migrateChanges, 1);
+    checkMigrate(project, ImmutableList.of(RefNames.changeMetaRef(c.getId())));
 
     // Comment content is the same.
     notes = newNotes(c);
@@ -265,7 +266,11 @@
         .containsExactly(otherCommentPs1.key, true);
 
     ChangeNotes oldNotes = notes;
-    migrate(allUsers, migrator::migrateDrafts, 2);
+    checkMigrate(
+        allUsers,
+        ImmutableList.of(
+            RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()),
+            RefNames.refsDraftComments(c.getId(), otherUser.getAccountId())));
     assertNoDifferences(notes, oldNotes);
 
     // Migration doesn't touch change ref.
@@ -366,7 +371,7 @@
         .containsExactly(ps1Comment.key, true, ps2Comment.key, false, ps3Comment.key, true);
 
     ChangeNotes oldNotes = notes;
-    migrate(project, migrator::migrateChanges, 1);
+    checkMigrate(project, ImmutableList.of(RefNames.changeMetaRef(c.getId())));
     assertNoDifferences(notes, oldNotes);
 
     // Comment content is the same.
@@ -391,30 +396,12 @@
         .containsExactly(ps1Comment.key, false, ps2Comment.key, false, ps3Comment.key, false);
   }
 
-  @FunctionalInterface
-  interface MigrateFunction {
-    boolean call(
-        Project.NameKey project,
-        Repository repo,
-        RevWalk rw,
-        ObjectInserter ins,
-        BatchRefUpdate bru)
-        throws Exception;
-  }
+  private void checkMigrate(Project.NameKey project, List<String> expectedRefs) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
 
-  private void migrate(Project.NameKey project, MigrateFunction func, int expectedCommands)
-      throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo);
-        ObjectInserter ins = repo.newObjectInserter()) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      bru.setAllowNonFastForwards(true);
-      assertThat(func.call(project, repo, rw, ins, bru)).isTrue();
-      assertThat(bru.getCommands()).hasSize(expectedCommands);
-      if (!bru.getCommands().isEmpty()) {
-        ins.flush();
-        RefUpdateUtil.executeChecked(bru, rw);
-      }
+      assertThat(progress.ok).isTrue();
+      assertThat(progress.refsUpdated).isEqualTo(expectedRefs);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index db7035b..963546f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -20,10 +20,10 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestChanges;
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index bc3c9a9..3d9d661 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -541,17 +541,19 @@
 
   @Test
   public void withSecondaryEmailsWithoutModifyAccountCapability() throws Exception {
-    AccountInfo user = newAccount("myuser", "My User", "abc@example.com", true);
+    AccountInfo user = newAccount("myuser", "My User", "other@example.com", true);
+
+    AccountInfo otherUser = newAccount("otheruser", "Other User", "abc@example.com", true);
     String[] secondaryEmails = new String[] {"dfg@example.com", "hij@example.com"};
-    addEmails(user, secondaryEmails);
+    addEmails(otherUser, secondaryEmails);
 
     requestContext.setContext(newRequestContext(new Account.Id(user._accountId)));
 
-    List<AccountInfo> result = newQuery(user.username).withSuggest(true).get();
+    List<AccountInfo> result = newQuery(otherUser.username).withSuggest(true).get();
     assertThat(result.get(0).secondaryEmails).isNull();
 
     exception.expect(AuthException.class);
-    newQuery(user.username).withOption(ListAccountsOption.ALL_EMAILS).get();
+    newQuery(otherUser.username).withOption(ListAccountsOption.ALL_EMAILS).get();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index cf85aeb..0ff16bb 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -831,7 +831,13 @@
     ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
     Change change4 = insert(repo, ins4);
 
-    Change change5 = insert(repo, newChange(repo));
+    ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
+    Change change5 = insert(repo, ins5);
+
+    ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
+    Change change6 = insert(repo, ins6);
+
+    Change change_no_topic = insert(repo, newChange(repo));
 
     assertQuery("intopic:foo");
     assertQuery("intopic:feature1", change1);
@@ -839,8 +845,9 @@
     assertQuery("topic:feature2", change2);
     assertQuery("intopic:feature2", change4, change3, change2);
     assertQuery("intopic:fixup", change4);
-    assertQuery("topic:\"\"", change5);
-    assertQuery("intopic:\"\"", change5);
+    assertQuery("intopic:gerrit", change6, change5);
+    assertQuery("topic:\"\"", change_no_topic);
+    assertQuery("intopic:\"\"", change_no_topic);
   }
 
   @Test
@@ -899,6 +906,14 @@
   }
 
   @Test
+  public void byMessageSubstring() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create());
+    Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+    assertQuery("message:gerrit", change1);
+  }
+
+  @Test
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
     TestRepository<Repo> repo = createProject("repo");
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index e34746c..2eec006 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -15,15 +15,21 @@
 package com.google.gerrit.server.query.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects.QueryRequest;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
@@ -38,7 +44,6 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -83,7 +88,7 @@
 
   @Inject protected OneOffRequestContext oneOffRequestContext;
 
-  @Inject protected InternalAccountQuery internalAccountQuery;
+  @Inject protected ProjectIndexCollection indexes;
 
   @Inject protected AllProjectsName allProjects;
 
@@ -211,6 +216,30 @@
   }
 
   @Test
+  public void byState() throws Exception {
+    assume().that(getSchemaVersion() >= 2).isTrue();
+
+    ProjectInfo project1 = createProjectWithState(name("project1"), ProjectState.ACTIVE);
+    ProjectInfo project2 = createProjectWithState(name("project2"), ProjectState.READ_ONLY);
+    assertQuery("state:active", project1);
+    assertQuery("state:read-only", project2);
+  }
+
+  @Test
+  public void byState_emptyQuery() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("state operator requires a value");
+    assertQuery("state:\"\"");
+  }
+
+  @Test
+  public void byState_badQuery() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("state operator must be either 'active' or 'read-only'");
+    assertQuery("state:bla");
+  }
+
+  @Test
   public void byDefaultField() throws Exception {
     ProjectInfo project1 = createProject(name("foo-project"));
     ProjectInfo project2 = createProject(name("project2"));
@@ -291,6 +320,14 @@
     return gApi.projects().create(in).get();
   }
 
+  protected ProjectInfo createProjectWithState(String name, ProjectState state) throws Exception {
+    ProjectInfo info = createProject(name);
+    ConfigInput config = new ConfigInput();
+    config.state = state;
+    gApi.projects().name(info.name).config(config);
+    return info;
+  }
+
   protected ProjectInfo getProject(Project.NameKey nameKey) throws Exception {
     return gApi.projects().name(nameKey.get()).get();
   }
@@ -354,6 +391,14 @@
     return b.toString();
   }
 
+  protected int getSchemaVersion() {
+    return getSchema().getVersion();
+  }
+
+  protected Schema<ProjectData> getSchema() {
+    return indexes.getSearchIndex().getSchema();
+  }
+
   protected static Iterable<String> names(ProjectInfo... projects) {
     return names(Arrays.asList(projects));
   }
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index eaa3df3..f0c455e 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -9,6 +9,8 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/index",
+        "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
diff --git a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
index a6178ac..ead824f 100644
--- a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
+++ b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
@@ -542,6 +542,8 @@
   public void combineWithBatchGroupNameNotes() throws Exception {
     AccountGroup g1 = newGroup("a");
     AccountGroup g2 = newGroup("b");
+    GroupReference gr1 = new GroupReference(g1.getGroupUUID(), g1.getName());
+    GroupReference gr2 = new GroupReference(g2.getGroupUUID(), g2.getName());
 
     GroupBundle b1 = builder().group(g1).build();
     GroupBundle b2 = builder().group(g2).build();
@@ -551,8 +553,7 @@
     rebuilder.rebuild(repo, b1, bru);
     rebuilder.rebuild(repo, b2, bru);
     try (ObjectInserter inserter = repo.newObjectInserter()) {
-      ImmutableList<GroupReference> refs =
-          ImmutableList.of(GroupReference.forGroup(g1), GroupReference.forGroup(g2));
+      ImmutableList<GroupReference> refs = ImmutableList.of(gr1, gr2);
       GroupNameNotes.updateAllGroups(repo, inserter, bru, refs, newPersonIdent());
       inserter.flush();
     }
@@ -569,9 +570,7 @@
     assertMigratedCleanly(reload(g1), b1);
     assertMigratedCleanly(reload(g2), b2);
 
-    GroupReference group1 = GroupReference.forGroup(g1);
-    GroupReference group2 = GroupReference.forGroup(g2);
-    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(group1, group2);
+    assertThat(GroupNameNotes.loadAllGroups(repo)).containsExactly(gr1, gr2);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
index 39afcac..808eca8 100644
--- a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -34,11 +34,4 @@
     assertEquals(0xdeadbeef, IdGenerator.unmix(IdGenerator.mix(0xdeadbeef)));
     assertEquals(0x0b966b11, IdGenerator.unmix(IdGenerator.mix(0x0b966b11)));
   }
-
-  @Test
-  public void format() {
-    assertEquals("0000000f", IdGenerator.format(0xf));
-    assertEquals("801234ab", IdGenerator.format(0x801234ab));
-    assertEquals("deadbeef", IdGenerator.format(0xdeadbeef));
-  }
 }
diff --git a/javatests/com/google/gerrit/server/util/git/BUILD b/javatests/com/google/gerrit/server/util/git/BUILD
new file mode 100644
index 0000000..928705c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/util/git/BUILD
@@ -0,0 +1,32 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "git_tests",
+    size = "small",
+    srcs = glob(
+        ["**/*.java"],
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server/util/git",
+        "//java/com/google/gerrit/truth",
+        "//java/org/eclipse/jgit:server",
+        "//lib:grappa",
+        "//lib:gson",
+        "//lib:guava",
+        "//lib:guava-retrying",
+        "//lib:gwtorm",
+        "//lib:protobuf",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/commons:codec",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/truth",
+        "//lib/truth:truth-java8-extension",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
similarity index 91%
rename from javatests/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
rename to javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
index d0225c7..0ec9b38 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
+++ b/javatests/com/google/gerrit/server/util/git/SubmoduleSectionParserTest.java
@@ -12,26 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.git;
+package com.google.gerrit.server.util.git;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.server.util.SubmoduleSectionParser;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-public class SubmoduleSectionParserIT extends AbstractDaemonTest {
+public class SubmoduleSectionParserTest {
   private static final String THIS_SERVER = "http://localhost/";
 
   @Test
   public void followMasterBranch() throws Exception {
-    Project.NameKey p = createProject("a");
+    Project.NameKey p = new Project.NameKey("proj");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -56,7 +54,7 @@
 
   @Test
   public void followMatchingBranch() throws Exception {
-    Project.NameKey p = createProject("a");
+    Project.NameKey p = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -92,7 +90,7 @@
 
   @Test
   public void followAnotherBranch() throws Exception {
-    Project.NameKey p = createProject("a");
+    Project.NameKey p = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -117,7 +115,7 @@
 
   @Test
   public void withAnotherURI() throws Exception {
-    Project.NameKey p = createProject("a");
+    Project.NameKey p = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -142,7 +140,7 @@
 
   @Test
   public void withSlashesInProjectName() throws Exception {
-    Project.NameKey p = createProject("project/with/slashes/a");
+    Project.NameKey p = new Project.NameKey("project/with/slashes/a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -167,7 +165,7 @@
 
   @Test
   public void withSlashesInPath() throws Exception {
-    Project.NameKey p = createProject("a");
+    Project.NameKey p = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -192,8 +190,8 @@
 
   @Test
   public void withMoreSections() throws Exception {
-    Project.NameKey p1 = createProject("a");
-    Project.NameKey p2 = createProject("b");
+    Project.NameKey p1 = new Project.NameKey("a");
+    Project.NameKey p2 = new Project.NameKey("b");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -225,8 +223,8 @@
 
   @Test
   public void withSubProjectFound() throws Exception {
-    Project.NameKey p1 = createProject("a/b");
-    Project.NameKey p2 = createProject("b");
+    Project.NameKey p1 = new Project.NameKey("a/b");
+    Project.NameKey p2 = new Project.NameKey("b");
     Config cfg = new Config();
     cfg.fromText(
         "\n"
@@ -258,10 +256,10 @@
 
   @Test
   public void withAnInvalidSection() throws Exception {
-    Project.NameKey p1 = createProject("a");
-    Project.NameKey p2 = createProject("b");
-    Project.NameKey p3 = createProject("d");
-    Project.NameKey p4 = createProject("e");
+    Project.NameKey p1 = new Project.NameKey("a");
+    Project.NameKey p2 = new Project.NameKey("b");
+    Project.NameKey p3 = new Project.NameKey("d");
+    Project.NameKey p4 = new Project.NameKey("e");
     Config cfg = new Config();
     cfg.fromText(
         "\n"
@@ -328,7 +326,7 @@
 
   @Test
   public void withSectionToOtherServer() throws Exception {
-    Project.NameKey p1 = createProject("a");
+    Project.NameKey p1 = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -349,7 +347,7 @@
 
   @Test
   public void withRelativeURI() throws Exception {
-    Project.NameKey p1 = createProject("a");
+    Project.NameKey p1 = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -374,7 +372,7 @@
 
   @Test
   public void withDeepRelativeURI() throws Exception {
-    Project.NameKey p1 = createProject("a");
+    Project.NameKey p1 = new Project.NameKey("a");
     Config cfg = new Config();
     cfg.fromText(
         ""
@@ -400,7 +398,7 @@
 
   @Test
   public void withOverlyDeepRelativeURI() throws Exception {
-    Project.NameKey p1 = createProject("nested/a");
+    Project.NameKey p1 = new Project.NameKey("nested/a");
     Config cfg = new Config();
     cfg.fromText(
         ""
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
index 5088a05..593caa3 100644
--- a/lib/codemirror/cm.bzl
+++ b/lib/codemirror/cm.bzl
@@ -234,126 +234,142 @@
                         DIFF_MATCH_PATCH_VERSION)
 
 def pkg_cm():
-  for archive, suffix, top, license in [
-      ('@codemirror-original-gwt//jar', '', TOP, LICENSE),
-      ('@codemirror-minified-gwt//jar', '_r', TOP_MINIFIED, LICENSE_MINIFIED)
-  ]:
-    # Main JavaScript and addons
-    genrule2(
-      name = 'cm' + suffix,
-      cmd = ' && '.join([
-          "echo '/** @license' >$@",
-          'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
-          "echo '*/' >>$@",
-        ] +
-        ['unzip -p $(location %s) %s/%s >>$@' % (archive, top, n) for n in CM_JS] +
-        ['unzip -p $(location %s) %s/addon/%s >>$@' % (archive, top, n)
-         for n in CM_ADDONS]
-      ),
-      tools = [archive],
-      outs = ['cm%s.js' % suffix],
-    )
+    for archive, suffix, top, license in [
+        ("@codemirror-original-gwt//jar", "", TOP, LICENSE),
+        ("@codemirror-minified-gwt//jar", "_r", TOP_MINIFIED, LICENSE_MINIFIED),
+    ]:
+        # Main JavaScript and addons
+        genrule2(
+            name = "cm" + suffix,
+            cmd = " && ".join(
+                [
+                    "echo '/** @license' >$@",
+                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
+                    "echo '*/' >>$@",
+                ] +
+                ["unzip -p $(location %s) %s/%s >>$@" % (archive, top, n) for n in CM_JS] +
+                [
+                    "unzip -p $(location %s) %s/addon/%s >>$@" % (archive, top, n)
+                    for n in CM_ADDONS
+                ],
+            ),
+            tools = [archive],
+            outs = ["cm%s.js" % suffix],
+        )
 
-    # Main CSS
-    genrule2(
-      name = 'css' + suffix,
-      cmd = ' && '.join([
-          "echo '/** @license' >$@",
-          'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
-          "echo '*/' >>$@",
-        ] +
-        ['unzip -p $(location %s) %s/%s >>$@' % (archive, top, n)
-         for n in CM_CSS]
-      ),
-      tools = [archive],
-      outs = ['cm%s.css' % suffix],
-    )
+        # Main CSS
+        genrule2(
+            name = "css" + suffix,
+            cmd = " && ".join(
+                [
+                    "echo '/** @license' >$@",
+                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
+                    "echo '*/' >>$@",
+                ] +
+                [
+                    "unzip -p $(location %s) %s/%s >>$@" % (archive, top, n)
+                    for n in CM_CSS
+                ],
+            ),
+            tools = [archive],
+            outs = ["cm%s.css" % suffix],
+        )
 
-    # Modes
-    for n in CM_MODES:
-      genrule2(
-        name = 'mode_%s%s' % (n, suffix),
-        cmd = ' && '.join([
-            "echo '/** @license' >$@",
-            'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
-            "echo '*/' >>$@",
-            'unzip -p $(location %s) %s/mode/%s/%s.js >>$@' % (archive, top, n, n),
-          ]
-        ),
-        tools = [archive],
-        outs = ['mode_%s%s.js' % (n, suffix)],
-      )
+        # Modes
+        for n in CM_MODES:
+            genrule2(
+                name = "mode_%s%s" % (n, suffix),
+                cmd = " && ".join(
+                    [
+                        "echo '/** @license' >$@",
+                        "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
+                        "echo '*/' >>$@",
+                        "unzip -p $(location %s) %s/mode/%s/%s.js >>$@" % (archive, top, n, n),
+                    ],
+                ),
+                tools = [archive],
+                outs = ["mode_%s%s.js" % (n, suffix)],
+            )
 
-    # Themes
-    for n in CM_THEMES:
-      genrule2(
-        name = 'theme_%s%s' % (n, suffix),
-        cmd = ' && '.join([
-            "echo '/** @license' >$@",
-            'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
-            "echo '*/' >>$@",
-            'unzip -p $(location %s) %s/theme/%s.css >>$@' % (archive, top, n)
-          ]
-        ),
-        tools = [archive],
-        outs = ['theme_%s%s.css' % (n, suffix)],
-      )
+        # Themes
+        for n in CM_THEMES:
+            genrule2(
+                name = "theme_%s%s" % (n, suffix),
+                cmd = " && ".join(
+                    [
+                        "echo '/** @license' >$@",
+                        "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
+                        "echo '*/' >>$@",
+                        "unzip -p $(location %s) %s/theme/%s.css >>$@" % (archive, top, n),
+                    ],
+                ),
+                tools = [archive],
+                outs = ["theme_%s%s.css" % (n, suffix)],
+            )
 
-    # Merge Addon bundled with diff-match-patch
-    genrule2(
-      name = 'addon_merge_with_diff_match_patch%s' % suffix,
-      cmd = ' && '.join([
-          "echo '/** @license' >$@",
-          'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
-          "echo '*/\n' >>$@",
-          "echo '// The google-diff-match-patch library is from https://repo1.maven.org/maven2/org/webjars/google-diff-match-patch/%s/google-diff-match-patch-%s.jar\n' >> $@" % (DIFF_MATCH_PATCH_VERSION, DIFF_MATCH_PATCH_VERSION),
-          "echo '/** @license' >>$@",
-          "echo 'LICENSE-Apache2.0' >>$@",
-          "echo '*/' >>$@",
-          'unzip -p $(location @diff-match-patch//jar) %s/diff_match_patch.js >>$@' % DIFF_MATCH_PATCH_TOP,
-          "echo ';' >> $@",
-          'unzip -p $(location %s) %s/addon/merge/merge.js >>$@' % (archive, top)
-        ]
-      ),
-      tools = [
-        '@diff-match-patch//jar',
-        # dependency just for license tracking.
-        ':diff-match-patch',
-        archive,
-        "//lib:LICENSE-Apache2.0",
-      ],
-      outs = ['addon_merge_with_diff_match_patch%s.js' % suffix],
-    )
+        # Merge Addon bundled with diff-match-patch
+        genrule2(
+            name = "addon_merge_with_diff_match_patch%s" % suffix,
+            cmd = " && ".join(
+                [
+                    "echo '/** @license' >$@",
+                    "unzip -p $(location %s) %s/LICENSE >>$@" % (archive, top),
+                    "echo '*/\n' >>$@",
+                    "echo '// The google-diff-match-patch library is from https://repo1.maven.org/maven2/org/webjars/google-diff-match-patch/%s/google-diff-match-patch-%s.jar\n' >> $@" % (DIFF_MATCH_PATCH_VERSION, DIFF_MATCH_PATCH_VERSION),
+                    "echo '/** @license' >>$@",
+                    "echo 'LICENSE-Apache2.0' >>$@",
+                    "echo '*/' >>$@",
+                    "unzip -p $(location @diff-match-patch//jar) %s/diff_match_patch.js >>$@" % DIFF_MATCH_PATCH_TOP,
+                    "echo ';' >> $@",
+                    "unzip -p $(location %s) %s/addon/merge/merge.js >>$@" % (archive, top),
+                ],
+            ),
+            tools = [
+                "@diff-match-patch//jar",
+                # dependency just for license tracking.
+                ":diff-match-patch",
+                archive,
+                "//lib:LICENSE-Apache2.0",
+            ],
+            outs = ["addon_merge_with_diff_match_patch%s.js" % suffix],
+        )
 
-    # Jar packaging
-    genrule2(
-      name = 'jar' + suffix,
-      cmd = ' && '.join([
-        'cd $$TMP',
-        'mkdir -p net/codemirror/{addon,lib,mode,theme}',
-        'cp $$ROOT/$(location :css%s) net/codemirror/lib/cm.css' % suffix,
-        'cp $$ROOT/$(location :cm%s) net/codemirror/lib/cm.js' % suffix]
-        + ['cp $$ROOT/$(location :mode_%s%s) net/codemirror/mode/%s.js' % (n, suffix, n)
-           for n in CM_MODES]
-        + ['cp $$ROOT/$(location :theme_%s%s) net/codemirror/theme/%s.css' % (n, suffix, n)
-           for n in CM_THEMES]
-        + ['cp $$ROOT/$(location :addon_merge_with_diff_match_patch%s) net/codemirror/addon/merge_bundled.js' % suffix]
-        + ['zip -qr $$ROOT/$@ net/codemirror/{addon,lib,mode,theme}']),
-      tools = [
-        ':addon_merge_with_diff_match_patch%s' % suffix,
-        ':cm%s' % suffix,
-        ':css%s' % suffix,
-      ] + [
-        ':mode_%s%s' % (n, suffix) for n in CM_MODES
-      ] + [
-        ':theme_%s%s' % (n, suffix) for n in CM_THEMES
-      ],
-      outs = ['codemirror%s.jar' % suffix],
-    )
+        # Jar packaging
+        genrule2(
+            name = "jar" + suffix,
+            cmd = " && ".join([
+                                  "cd $$TMP",
+                                  "mkdir -p net/codemirror/{addon,lib,mode,theme}",
+                                  "cp $$ROOT/$(location :css%s) net/codemirror/lib/cm.css" % suffix,
+                                  "cp $$ROOT/$(location :cm%s) net/codemirror/lib/cm.js" % suffix,
+                              ] +
+                              [
+                                  "cp $$ROOT/$(location :mode_%s%s) net/codemirror/mode/%s.js" % (n, suffix, n)
+                                  for n in CM_MODES
+                              ] +
+                              [
+                                  "cp $$ROOT/$(location :theme_%s%s) net/codemirror/theme/%s.css" % (n, suffix, n)
+                                  for n in CM_THEMES
+                              ] +
+                              ["cp $$ROOT/$(location :addon_merge_with_diff_match_patch%s) net/codemirror/addon/merge_bundled.js" % suffix] +
+                              ["zip -qr $$ROOT/$@ net/codemirror/{addon,lib,mode,theme}"]),
+            tools = [
+                ":addon_merge_with_diff_match_patch%s" % suffix,
+                ":cm%s" % suffix,
+                ":css%s" % suffix,
+            ] + [
+                ":mode_%s%s" % (n, suffix)
+                for n in CM_MODES
+            ] + [
+                ":theme_%s%s" % (n, suffix)
+                for n in CM_THEMES
+            ],
+            outs = ["codemirror%s.jar" % suffix],
+        )
 
-    native.java_import(
-      name = 'codemirror' + suffix,
-      jars = [':jar%s' % suffix],
-      visibility = ['//visibility:public'],
-      data = [license],
-    )
+        native.java_import(
+            name = "codemirror" + suffix,
+            jars = [":jar%s" % suffix],
+            visibility = ["//visibility:public"],
+            data = [license],
+        )
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index adf2d4f8..384469c 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,8 +1,8 @@
-load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
+load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_CENTRAL", "MAVEN_LOCAL", "maven_jar")
 
 _JGIT_VERS = "5.0.1.201806211838-r"
 
-_DOC_VERS = "5.0.0.201806131550-r"  # Set to _JGIT_VERS unless using a snapshot
+_DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
@@ -13,13 +13,13 @@
 LOCAL_JGIT_REPO = ""
 
 def jgit_repos():
-  if LOCAL_JGIT_REPO:
-    native.local_repository(
-        name = "jgit",
-        path = LOCAL_JGIT_REPO,
-    )
-  else:
-    jgit_maven_repos()
+    if LOCAL_JGIT_REPO:
+        native.local_repository(
+            name = "jgit",
+            path = LOCAL_JGIT_REPO,
+        )
+    else:
+        jgit_maven_repos()
 
 def jgit_maven_repos():
     maven_jar(
@@ -52,15 +52,15 @@
     )
 
 def jgit_dep(name):
-  mapping = {
-      "@jgit-junit//jar": "@jgit//org.eclipse.jgit.junit:junit",
-      "@jgit-lib//jar:src": "@jgit//org.eclipse.jgit:libjgit-src.jar",
-      "@jgit-lib//jar": "@jgit//org.eclipse.jgit:jgit",
-      "@jgit-servlet//jar":"@jgit//org.eclipse.jgit.http.server:jgit-servlet",
-      "@jgit-archive//jar": "@jgit//org.eclipse.jgit.archive:jgit-archive",
-  }
+    mapping = {
+        "@jgit-junit//jar": "@jgit//org.eclipse.jgit.junit:junit",
+        "@jgit-lib//jar:src": "@jgit//org.eclipse.jgit:libjgit-src.jar",
+        "@jgit-lib//jar": "@jgit//org.eclipse.jgit:jgit",
+        "@jgit-servlet//jar": "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
+        "@jgit-archive//jar": "@jgit//org.eclipse.jgit.archive:jgit-archive",
+    }
 
-  if LOCAL_JGIT_REPO:
-    return mapping[name]
-  else:
-    return name
+    if LOCAL_JGIT_REPO:
+        return mapping[name]
+    else:
+        return name
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index 6b4e003..75c8277 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -7,138 +7,165 @@
 load("//tools/bzl:js.bzl", "bower_archive")
 
 def load_bower_archives():
-  bower_archive(
-    name = "accessibility-developer-tools",
-    package = "accessibility-developer-tools",
-    version = "2.12.0",
-    sha1 = "88ae82dcdeb6c658f76eff509d0ee425cae14d49")
-  bower_archive(
-    name = "async",
-    package = "async",
-    version = "1.5.2",
-    sha1 = "1ec975d3b3834646a7e3d4b7e68118b90ed72508")
-  bower_archive(
-    name = "chai",
-    package = "chai",
-    version = "3.5.0",
-    sha1 = "849ad3ee7c77506548b7b5db603a4e150b9431aa")
-  bower_archive(
-    name = "font-roboto",
-    package = "PolymerElements/font-roboto",
-    version = "1.1.0",
-    sha1 = "ab4218d87b9ce569d6282b01f7642e551879c3d5")
-  bower_archive(
-    name = "iron-a11y-announcer",
-    package = "PolymerElements/iron-a11y-announcer",
-    version = "1.0.6",
-    sha1 = "14aed1e1b300ea344e80362e875919ea3d104dcc")
-  bower_archive(
-    name = "iron-a11y-keys-behavior",
-    package = "PolymerElements/iron-a11y-keys-behavior",
-    version = "1.1.9",
-    sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465")
-  bower_archive(
-    name = "iron-behaviors",
-    package = "PolymerElements/iron-behaviors",
-    version = "1.0.18",
-    sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18")
-  bower_archive(
-    name = "iron-checked-element-behavior",
-    package = "PolymerElements/iron-checked-element-behavior",
-    version = "1.0.6",
-    sha1 = "93ad3554cec119d8c5732d1c722ad113e1866370")
-  bower_archive(
-    name = "iron-fit-behavior",
-    package = "PolymerElements/iron-fit-behavior",
-    version = "1.2.7",
-    sha1 = "01c485fbf898307029bbb72ac7e132db1570a842")
-  bower_archive(
-    name = "iron-flex-layout",
-    package = "PolymerElements/iron-flex-layout",
-    version = "1.3.9",
-    sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796")
-  bower_archive(
-    name = "iron-form-element-behavior",
-    package = "PolymerElements/iron-form-element-behavior",
-    version = "1.0.7",
-    sha1 = "7b5a79e02cc32f0918725dd26925d0df1e03ed12")
-  bower_archive(
-    name = "iron-menu-behavior",
-    package = "PolymerElements/iron-menu-behavior",
-    version = "2.1.1",
-    sha1 = "1504997f6eb9aec490b855dadee473cac064f38c")
-  bower_archive(
-    name = "iron-meta",
-    package = "PolymerElements/iron-meta",
-    version = "1.1.3",
-    sha1 = "f77eba3f6f6817f10bda33918bde8f963d450041")
-  bower_archive(
-    name = "iron-resizable-behavior",
-    package = "polymerelements/iron-resizable-behavior",
-    version = "1.0.6",
-    sha1 = "719c2a8a1a784f8aefcdeef41fcc2e5a03518d9e")
-  bower_archive(
-    name = "iron-validatable-behavior",
-    package = "PolymerElements/iron-validatable-behavior",
-    version = "1.1.2",
-    sha1 = "7111f34ff32e1510131dfbdb1eaa51bfa291e8be")
-  bower_archive(
-    name = "lodash",
-    package = "lodash",
-    version = "3.10.1",
-    sha1 = "2f207a8293c4c554bf6cf071241f7a00dc513d3a")
-  bower_archive(
-    name = "mocha",
-    package = "mocha",
-    version = "3.5.3",
-    sha1 = "c14f149821e4e96241b20f85134aa757b73038f1")
-  bower_archive(
-    name = "neon-animation",
-    package = "polymerelements/neon-animation",
-    version = "1.2.5",
-    sha1 = "588d289f779d02b21ce5b676e257bbd6155649e8")
-  bower_archive(
-    name = "paper-behaviors",
-    package = "PolymerElements/paper-behaviors",
-    version = "1.0.13",
-    sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7")
-  bower_archive(
-    name = "paper-icon-button",
-    package = "PolymerElements/paper-icon-button",
-    version = "2.2.0",
-    sha1 = "9525e76ef433428bb9d6ec4fa65c4ef83156a803")
-  bower_archive(
-    name = "paper-ripple",
-    package = "PolymerElements/paper-ripple",
-    version = "1.0.10",
-    sha1 = "21199db50d02b842da54bd6f4f1d1b10b474e893")
-  bower_archive(
-    name = "paper-styles",
-    package = "PolymerElements/paper-styles",
-    version = "1.3.1",
-    sha1 = "4ee9c692366949a754e0e39f8031aa60ce66f24d")
-  bower_archive(
-    name = "sinon-chai",
-    package = "sinon-chai",
-    version = "2.14.0",
-    sha1 = "78f0dc184efe47012a2b1b9a16a4289acf8300dc")
-  bower_archive(
-    name = "sinonjs",
-    package = "sinonjs",
-    version = "1.17.1",
-    sha1 = "a26a6aab7358807de52ba738770f6ac709afd240")
-  bower_archive(
-    name = "stacky",
-    package = "stacky",
-    version = "1.3.2",
-    sha1 = "d6c07a0112ab2e9677fe085933744466a89232fb")
-  bower_archive(
-    name = "web-animations-js",
-    package = "web-animations/web-animations-js",
-    version = "2.3.1",
-    sha1 = "2ba5548d36188fe54555eaad0a576de4b027661e")
-  bower_archive(
-    name = "webcomponentsjs",
-    package = "webcomponents/webcomponentsjs",
-    version = "0.7.24",
-    sha1 = "559227f8ee9db9bfbd81989f24510cc0c1bfc65c")
+    bower_archive(
+        name = "accessibility-developer-tools",
+        package = "accessibility-developer-tools",
+        version = "2.12.0",
+        sha1 = "88ae82dcdeb6c658f76eff509d0ee425cae14d49",
+    )
+    bower_archive(
+        name = "async",
+        package = "async",
+        version = "1.5.2",
+        sha1 = "1ec975d3b3834646a7e3d4b7e68118b90ed72508",
+    )
+    bower_archive(
+        name = "chai",
+        package = "chai",
+        version = "3.5.0",
+        sha1 = "849ad3ee7c77506548b7b5db603a4e150b9431aa",
+    )
+    bower_archive(
+        name = "font-roboto",
+        package = "PolymerElements/font-roboto",
+        version = "1.1.0",
+        sha1 = "ab4218d87b9ce569d6282b01f7642e551879c3d5",
+    )
+    bower_archive(
+        name = "iron-a11y-announcer",
+        package = "PolymerElements/iron-a11y-announcer",
+        version = "1.0.6",
+        sha1 = "14aed1e1b300ea344e80362e875919ea3d104dcc",
+    )
+    bower_archive(
+        name = "iron-a11y-keys-behavior",
+        package = "PolymerElements/iron-a11y-keys-behavior",
+        version = "1.1.9",
+        sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465",
+    )
+    bower_archive(
+        name = "iron-behaviors",
+        package = "PolymerElements/iron-behaviors",
+        version = "1.0.18",
+        sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18",
+    )
+    bower_archive(
+        name = "iron-checked-element-behavior",
+        package = "PolymerElements/iron-checked-element-behavior",
+        version = "1.0.6",
+        sha1 = "93ad3554cec119d8c5732d1c722ad113e1866370",
+    )
+    bower_archive(
+        name = "iron-fit-behavior",
+        package = "PolymerElements/iron-fit-behavior",
+        version = "1.2.7",
+        sha1 = "01c485fbf898307029bbb72ac7e132db1570a842",
+    )
+    bower_archive(
+        name = "iron-flex-layout",
+        package = "PolymerElements/iron-flex-layout",
+        version = "1.3.9",
+        sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796",
+    )
+    bower_archive(
+        name = "iron-form-element-behavior",
+        package = "PolymerElements/iron-form-element-behavior",
+        version = "1.0.7",
+        sha1 = "7b5a79e02cc32f0918725dd26925d0df1e03ed12",
+    )
+    bower_archive(
+        name = "iron-menu-behavior",
+        package = "PolymerElements/iron-menu-behavior",
+        version = "2.1.1",
+        sha1 = "1504997f6eb9aec490b855dadee473cac064f38c",
+    )
+    bower_archive(
+        name = "iron-meta",
+        package = "PolymerElements/iron-meta",
+        version = "1.1.3",
+        sha1 = "f77eba3f6f6817f10bda33918bde8f963d450041",
+    )
+    bower_archive(
+        name = "iron-resizable-behavior",
+        package = "polymerelements/iron-resizable-behavior",
+        version = "1.0.6",
+        sha1 = "719c2a8a1a784f8aefcdeef41fcc2e5a03518d9e",
+    )
+    bower_archive(
+        name = "iron-validatable-behavior",
+        package = "PolymerElements/iron-validatable-behavior",
+        version = "1.1.2",
+        sha1 = "7111f34ff32e1510131dfbdb1eaa51bfa291e8be",
+    )
+    bower_archive(
+        name = "lodash",
+        package = "lodash",
+        version = "3.10.1",
+        sha1 = "2f207a8293c4c554bf6cf071241f7a00dc513d3a",
+    )
+    bower_archive(
+        name = "mocha",
+        package = "mocha",
+        version = "3.5.3",
+        sha1 = "c14f149821e4e96241b20f85134aa757b73038f1",
+    )
+    bower_archive(
+        name = "neon-animation",
+        package = "polymerelements/neon-animation",
+        version = "1.2.5",
+        sha1 = "588d289f779d02b21ce5b676e257bbd6155649e8",
+    )
+    bower_archive(
+        name = "paper-behaviors",
+        package = "PolymerElements/paper-behaviors",
+        version = "1.0.13",
+        sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7",
+    )
+    bower_archive(
+        name = "paper-icon-button",
+        package = "PolymerElements/paper-icon-button",
+        version = "2.2.0",
+        sha1 = "9525e76ef433428bb9d6ec4fa65c4ef83156a803",
+    )
+    bower_archive(
+        name = "paper-ripple",
+        package = "PolymerElements/paper-ripple",
+        version = "1.0.10",
+        sha1 = "21199db50d02b842da54bd6f4f1d1b10b474e893",
+    )
+    bower_archive(
+        name = "paper-styles",
+        package = "PolymerElements/paper-styles",
+        version = "1.3.1",
+        sha1 = "4ee9c692366949a754e0e39f8031aa60ce66f24d",
+    )
+    bower_archive(
+        name = "sinon-chai",
+        package = "sinon-chai",
+        version = "2.14.0",
+        sha1 = "78f0dc184efe47012a2b1b9a16a4289acf8300dc",
+    )
+    bower_archive(
+        name = "sinonjs",
+        package = "sinonjs",
+        version = "1.17.1",
+        sha1 = "a26a6aab7358807de52ba738770f6ac709afd240",
+    )
+    bower_archive(
+        name = "stacky",
+        package = "stacky",
+        version = "1.3.2",
+        sha1 = "d6c07a0112ab2e9677fe085933744466a89232fb",
+    )
+    bower_archive(
+        name = "web-animations-js",
+        package = "web-animations/web-animations-js",
+        version = "2.3.1",
+        sha1 = "2ba5548d36188fe54555eaad0a576de4b027661e",
+    )
+    bower_archive(
+        name = "webcomponentsjs",
+        package = "webcomponents/webcomponentsjs",
+        version = "0.7.24",
+        sha1 = "559227f8ee9db9bfbd81989f24510cc0c1bfc65c",
+    )
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index dc16ccf..a540828 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -7,377 +7,377 @@
 load("//tools/bzl:js.bzl", "bower_component")
 
 def define_bower_components():
-  bower_component(
-    name = "accessibility-developer-tools",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "async",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "chai",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "es6-promise",
-    license = "//lib:LICENSE-es6-promise",
-    seed = True,
-  )
-  bower_component(
-    name = "fetch",
-    license = "//lib:LICENSE-fetch",
-    seed = True,
-  )
-  bower_component(
-    name = "font-roboto",
-    license = "//lib:LICENSE-polymer",
-  )
-  bower_component(
-    name = "iron-a11y-announcer",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-a11y-keys-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-autogrow-textarea",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-flex-layout",
-      ":iron-validatable-behavior",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-behaviors",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-keys-behavior",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "iron-checked-element-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-form-element-behavior",
-      ":iron-validatable-behavior",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "iron-dropdown",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-overlay-behavior",
-      ":iron-resizable-behavior",
-      ":neon-animation",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-fit-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-flex-layout",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-form-element-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-icon",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-flex-layout",
-      ":iron-meta",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-iconset-svg",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-meta",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-input",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-announcer",
-      ":iron-validatable-behavior",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-menu-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-keys-behavior",
-      ":iron-flex-layout",
-      ":iron-selector",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "iron-meta",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-overlay-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-keys-behavior",
-      ":iron-fit-behavior",
-      ":iron-resizable-behavior",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-resizable-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-  )
-  bower_component(
-    name = "iron-selector",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":polymer" ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-test-helpers",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-    deps = [ ":polymer" ],
-    seed = True,
-  )
-  bower_component(
-    name = "iron-validatable-behavior",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-meta",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "lodash",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "mocha",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "moment",
-    license = "//lib:LICENSE-moment",
-    seed = True,
-  )
-  bower_component(
-    name = "neon-animation",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-meta",
-      ":iron-resizable-behavior",
-      ":iron-selector",
-      ":polymer",
-      ":web-animations-js",
-    ],
-  )
-  bower_component(
-    name = "page",
-    license = "//lib:LICENSE-page.js",
-    seed = True,
-  )
-  bower_component(
-    name = "paper-behaviors",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-checked-element-behavior",
-      ":paper-ripple",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "paper-button",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-flex-layout",
-      ":paper-behaviors",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "paper-icon-button",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-icon",
-      ":paper-behaviors",
-      ":paper-styles",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "paper-input",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-keys-behavior",
-      ":iron-autogrow-textarea",
-      ":iron-behaviors",
-      ":iron-form-element-behavior",
-      ":iron-input",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "paper-item",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-flex-layout",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "paper-listbox",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-menu-behavior",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "paper-ripple",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-a11y-keys-behavior",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "paper-styles",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":font-roboto",
-      ":iron-flex-layout",
-      ":polymer",
-    ],
-  )
-  bower_component(
-    name = "paper-tabs",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-behaviors",
-      ":iron-flex-layout",
-      ":iron-icon",
-      ":iron-iconset-svg",
-      ":iron-menu-behavior",
-      ":iron-resizable-behavior",
-      ":paper-behaviors",
-      ":paper-icon-button",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "paper-toggle-button",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":iron-checked-element-behavior",
-      ":paper-behaviors",
-      ":paper-styles",
-      ":polymer",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "polymer-resin",
-    license = "//lib:LICENSE-polymer",
-    deps = [
-      ":polymer",
-      ":webcomponentsjs",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "polymer",
-    license = "//lib:LICENSE-polymer",
-    deps = [ ":webcomponentsjs" ],
-    seed = True,
-  )
-  bower_component(
-    name = "promise-polyfill",
-    license = "//lib:LICENSE-promise-polyfill",
-    deps = [ ":polymer" ],
-    seed = True,
-  )
-  bower_component(
-    name = "sinon-chai",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "sinonjs",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "stacky",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-  )
-  bower_component(
-    name = "test-fixture",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-    seed = True,
-  )
-  bower_component(
-    name = "web-animations-js",
-    license = "//lib:LICENSE-Apache2.0",
-  )
-  bower_component(
-    name = "web-component-tester",
-    license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
-    deps = [
-      ":accessibility-developer-tools",
-      ":async",
-      ":chai",
-      ":lodash",
-      ":mocha",
-      ":sinon-chai",
-      ":sinonjs",
-      ":stacky",
-      ":test-fixture",
-    ],
-    seed = True,
-  )
-  bower_component(
-    name = "webcomponentsjs",
-    license = "//lib:LICENSE-polymer",
-  )
+    bower_component(
+        name = "accessibility-developer-tools",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "async",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "chai",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "es6-promise",
+        license = "//lib:LICENSE-es6-promise",
+        seed = True,
+    )
+    bower_component(
+        name = "fetch",
+        license = "//lib:LICENSE-fetch",
+        seed = True,
+    )
+    bower_component(
+        name = "font-roboto",
+        license = "//lib:LICENSE-polymer",
+    )
+    bower_component(
+        name = "iron-a11y-announcer",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-a11y-keys-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-autogrow-textarea",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-flex-layout",
+            ":iron-validatable-behavior",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-behaviors",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-keys-behavior",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "iron-checked-element-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-form-element-behavior",
+            ":iron-validatable-behavior",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "iron-dropdown",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-overlay-behavior",
+            ":iron-resizable-behavior",
+            ":neon-animation",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-fit-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-flex-layout",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-form-element-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-icon",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-flex-layout",
+            ":iron-meta",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-iconset-svg",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-meta",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-input",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-announcer",
+            ":iron-validatable-behavior",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-menu-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-keys-behavior",
+            ":iron-flex-layout",
+            ":iron-selector",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "iron-meta",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-overlay-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-keys-behavior",
+            ":iron-fit-behavior",
+            ":iron-resizable-behavior",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-resizable-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+    )
+    bower_component(
+        name = "iron-selector",
+        license = "//lib:LICENSE-polymer",
+        deps = [":polymer"],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-test-helpers",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+        deps = [":polymer"],
+        seed = True,
+    )
+    bower_component(
+        name = "iron-validatable-behavior",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-meta",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "lodash",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "mocha",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "moment",
+        license = "//lib:LICENSE-moment",
+        seed = True,
+    )
+    bower_component(
+        name = "neon-animation",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-meta",
+            ":iron-resizable-behavior",
+            ":iron-selector",
+            ":polymer",
+            ":web-animations-js",
+        ],
+    )
+    bower_component(
+        name = "page",
+        license = "//lib:LICENSE-page.js",
+        seed = True,
+    )
+    bower_component(
+        name = "paper-behaviors",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-checked-element-behavior",
+            ":paper-ripple",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "paper-button",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-flex-layout",
+            ":paper-behaviors",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "paper-icon-button",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-icon",
+            ":paper-behaviors",
+            ":paper-styles",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "paper-input",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-keys-behavior",
+            ":iron-autogrow-textarea",
+            ":iron-behaviors",
+            ":iron-form-element-behavior",
+            ":iron-input",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "paper-item",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-flex-layout",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "paper-listbox",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-menu-behavior",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "paper-ripple",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-a11y-keys-behavior",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "paper-styles",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":font-roboto",
+            ":iron-flex-layout",
+            ":polymer",
+        ],
+    )
+    bower_component(
+        name = "paper-tabs",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-behaviors",
+            ":iron-flex-layout",
+            ":iron-icon",
+            ":iron-iconset-svg",
+            ":iron-menu-behavior",
+            ":iron-resizable-behavior",
+            ":paper-behaviors",
+            ":paper-icon-button",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "paper-toggle-button",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":iron-checked-element-behavior",
+            ":paper-behaviors",
+            ":paper-styles",
+            ":polymer",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "polymer-resin",
+        license = "//lib:LICENSE-polymer",
+        deps = [
+            ":polymer",
+            ":webcomponentsjs",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "polymer",
+        license = "//lib:LICENSE-polymer",
+        deps = [":webcomponentsjs"],
+        seed = True,
+    )
+    bower_component(
+        name = "promise-polyfill",
+        license = "//lib:LICENSE-promise-polyfill",
+        deps = [":polymer"],
+        seed = True,
+    )
+    bower_component(
+        name = "sinon-chai",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "sinonjs",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "stacky",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+    )
+    bower_component(
+        name = "test-fixture",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+        seed = True,
+    )
+    bower_component(
+        name = "web-animations-js",
+        license = "//lib:LICENSE-Apache2.0",
+    )
+    bower_component(
+        name = "web-component-tester",
+        license = "//lib:LICENSE-DO_NOT_DISTRIBUTE",
+        deps = [
+            ":accessibility-developer-tools",
+            ":async",
+            ":chai",
+            ":lodash",
+            ":mocha",
+            ":sinon-chai",
+            ":sinonjs",
+            ":stacky",
+            ":test-fixture",
+        ],
+        seed = True,
+    )
+    bower_component(
+        name = "webcomponentsjs",
+        license = "//lib:LICENSE-polymer",
+    )
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl
index d905ad8..4d4dd3a 100644
--- a/lib/prolog/prolog.bzl
+++ b/lib/prolog/prolog.bzl
@@ -13,22 +13,22 @@
 # limitations under the License.
 
 def prolog_cafe_library(
-    name,
-    srcs,
-    deps = [],
-    **kwargs):
-  native.genrule(
-    name = name + '__pl2j',
-    cmd = '$(location //lib/prolog:compiler-bin) ' +
-      '$$(dirname $@) $@ ' +
-      '$(SRCS)',
-    srcs = srcs,
-    tools = ['//lib/prolog:compiler-bin'],
-    outs = [ name + '.srcjar' ],
-  )
-  native.java_library(
-    name = name,
-    srcs = [':' + name + '__pl2j'],
-    deps = ['//lib/prolog:runtime-neverlink'] + deps,
-    **kwargs
-  )
+        name,
+        srcs,
+        deps = [],
+        **kwargs):
+    native.genrule(
+        name = name + "__pl2j",
+        cmd = "$(location //lib/prolog:compiler-bin) " +
+              "$$(dirname $@) $@ " +
+              "$(SRCS)",
+        srcs = srcs,
+        tools = ["//lib/prolog:compiler-bin"],
+        outs = [name + ".srcjar"],
+    )
+    native.java_library(
+        name = name,
+        srcs = [":" + name + "__pl2j"],
+        deps = ["//lib/prolog:runtime-neverlink"] + deps,
+        **kwargs
+    )
diff --git a/plugins/BUILD b/plugins/BUILD
index 4cc982a..a623ff7 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -19,6 +19,7 @@
 
 PLUGIN_API = [
     "//java/com/google/gerrit/server",
+    "//java/com/google/gerrit/server/ioutil",
     "//java/com/google/gerrit/server/restapi",
     "//java/com/google/gerrit/pgm/init/api",
     "//java/com/google/gerrit/httpd",
@@ -37,6 +38,7 @@
     "//java/com/google/gerrit/metrics/dropwizard",
     "//java/com/google/gerrit/reviewdb:server",
     "//java/com/google/gerrit/util/http",
+    "//lib/commons:compress",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
     "//lib/dropwizard:dropwizard-core",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 53dccff..318504d 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 53dccff17c029459999ff70ac886b80626af634b
+Subproject commit 318504d0eb74f2f9c2967e9865ace6d5b57638c6
diff --git a/plugins/external_plugin_deps.bzl b/plugins/external_plugin_deps.bzl
index 391f920..1f7c020 100644
--- a/plugins/external_plugin_deps.bzl
+++ b/plugins/external_plugin_deps.bzl
@@ -1,2 +1,2 @@
 def external_plugin_deps():
-    pass
\ No newline at end of file
+    pass
diff --git a/plugins/replication b/plugins/replication
index 3a47f8c..1086fac 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 3a47f8c11ebdbcbc65fc6a58c35d18f1f3c3a74b
+Subproject commit 1086faccd0cf2aa53977854767fdc77f048b0253
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
index 61df877..45bc5f6 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -45,7 +45,7 @@
       .header,
       #deletedContainer {
         align-items: center;
-        background: #f6f6f6;
+        background: var(--table-header-background-color);
         border-bottom: 1px dotted var(--border-color);
         display: flex;
         justify-content: space-between;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 704974d..fd6eb80 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -243,6 +243,22 @@
                 </span>
               </section>
               <section>
+                <span class="title">
+                  Set new changes to "work in progress" by default</span>
+                <span class="value">
+                  <gr-select
+                      id="setAllNewChangesWorkInProgressByDefaultSelect"
+                      bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
+                    <select disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                          items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                    </select>
+                  </gr-select>
+                </span>
+              </section>
+              <section>
                 <span class="title">Maximum Git object size limit</span>
                 <span class="value">
                   <input
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 6ea7cf3..7eb2b6c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -171,11 +171,14 @@
     </td>
     <td class="cell project"
         hidden$="[[isColumnHidden('Project', visibleChangeTableColumns)]]">
-      <a class="fullProject" href$="[[_computeProjectURL(change.project)]]">
-        [[change.project]]
+      <a class="fullProject" href$="[[_computeProjectURL(change)]]">
+        [[_computeProjectDisplay(change)]]
       </a>
-      <a class="truncatedProject" href$="[[_computeProjectURL(change.project)]]">
-        [[_computeTruncatedProject(change.project)]]
+      <a
+          class="truncatedProject"
+          href$="[[_computeProjectURL(change)]]"
+          title$="[[_computeProjectDisplay(change)]]">
+        [[_computeProjectDisplay(change, 'true')]]
       </a>
     </td>
     <td class="cell branch"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 259580b..b73a237 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -122,22 +122,36 @@
       return '';
     },
 
-    _computeProjectURL(project) {
-      return Gerrit.Nav.getUrlForProjectChanges(project, true);
+    _computeProjectURL(change) {
+      return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
+          change.internalHost);
     },
 
     _computeProjectBranchURL(change) {
-      return Gerrit.Nav.getUrlForBranch(change.branch, change.project);
+      return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
+          change.internalHost);
     },
 
     _computeTopicURL(change) {
       if (!change.topic) { return ''; }
-      return Gerrit.Nav.getUrlForTopic(change.topic);
+      return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost);
     },
 
-    _computeTruncatedProject(project) {
-      if (!project) { return ''; }
-      return this.truncatePath(project, 2);
+    /**
+     * Computes the display string for the project column. If there is a host
+     * specified in the change detail, the string will be prefixed with it.
+     *
+     * @param {!Object} change
+     * @param {string=} truncate whether or not the project name should be
+     *     truncated. If this value is truthy, the name will be truncated.
+     * @return {string}
+     */
+    _computeProjectDisplay(change, truncate) {
+      if (!change || !change.project) { return ''; }
+      let str = '';
+      if (change.internalHost) { str += change.internalHost + '/'; }
+      str += truncate ? this.truncatePath(change.project, 2) : change.project;
+      return str;
     },
 
     _computeAccountStatusString(account) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 81e1034..f4c66c7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -37,8 +37,10 @@
 <script>
   suite('gr-change-list-item tests', () => {
     let element;
+    let sandbox;
 
     setup(() => {
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
         getLoggedIn() { return Promise.resolve(false); },
@@ -46,6 +48,8 @@
       element = fixture('basic');
     });
 
+    teardown(() => { sandbox.restore(); });
+
     test('computed fields', () => {
       assert.equal(element._computeLabelClass({labels: {}}),
           'cell label u-gray-background');
@@ -249,5 +253,39 @@
         deletions: 999,
       }), 'XL');
     });
+
+    test('change params passed to gr-navigation', () => {
+      sandbox.stub(Gerrit.Nav);
+      const change = {
+        internalHost: 'test-host',
+        project: 'test-repo',
+        topic: 'test-topic',
+        branch: 'test-branch',
+      };
+      element.change = change;
+      flushAsynchronousOperations();
+
+      assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]);
+      assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args,
+          [change.project, true, change.internalHost]);
+      assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args,
+          [change.branch, change.project, null, change.internalHost]);
+      assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args,
+          [change.topic, change.internalHost]);
+    });
+
+    test('_computeProjectDisplay', () => {
+      const change = {
+        project: 'a/test/repo',
+        internalHost: 'host',
+      };
+      assert.equal(element._computeProjectDisplay(change), 'host/a/test/repo');
+      assert.equal(element._computeProjectDisplay(change, true),
+          'host/…/test/repo');
+      delete change.internalHost;
+      assert.equal(element._computeProjectDisplay(change), 'a/test/repo');
+      assert.equal(element._computeProjectDisplay(change, true),
+          '…/test/repo');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 3a7ff10..fc00299 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -86,7 +86,8 @@
           changes="{{_changes}}"
           preferences="[[preferences]]"
           selected-index="{{viewState.selectedChangeIndex}}"
-          show-star="[[_loggedIn]]"></gr-change-list>
+          show-star="[[_loggedIn]]"
+          on-toggle-star="_handleToggleStar"></gr-change-list>
       <nav class$="[[_computeNavClass(_loading)]]">
           Page [[_computePage(_offset, _changesPerPage)]]
           <a id="prevArrow"
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 2a05a2f..9b099cd 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
@@ -263,5 +263,10 @@
     _computeLoggedIn(account) {
       return !!(account && Object.keys(account).length > 0);
     },
+
+    _handleToggleStar(e) {
+      this.$.restAPI.saveChangeStarred(e.detail.change._number,
+          e.detail.starred);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 9930bf5..33bc338 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -18,10 +18,9 @@
   'use strict';
 
   const NUMBER_FIXED_COLUMNS = 3;
-
   const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
-
   const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+  const MAX_SHORTCUT_CHARS = 5;
 
   Polymer({
     is: 'gr-change-list',
@@ -185,10 +184,12 @@
       if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
         labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
       }
-      return labelName.split('-').reduce((a, i) => {
-        if (!i) { return a; }
-        return a + i[0].toUpperCase();
-      }, '');
+      return labelName.split('-')
+          .reduce((a, i) => {
+            if (!i) { return a; }
+            return a + i[0].toUpperCase();
+          }, '')
+          .slice(0, MAX_SHORTCUT_CHARS);
     },
 
     _changesChanged(changes) {
@@ -306,10 +307,7 @@
       }
 
       const changeEl = changeEls[index];
-      const change = changeEl.change;
-      const newVal = !change.starred;
-      changeEl.set('change.starred', newVal);
-      this.$.restAPI.saveChangeStarred(change._number, newVal);
+      changeEl.$$('gr-change-star').toggleStar();
     },
 
     _changeForIndex(index) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 9ce5764..39d7ab1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -132,6 +132,8 @@
           'Some-Special-Label-7'), 'SSL7');
       assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
           'TMD');
+      assert.equal(element._computeLabelShortcut(
+          'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
     });
 
     test('colspans', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index 1935962..1b21543 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -60,7 +60,8 @@
           account="[[account]]"
           preferences="[[preferences]]"
           selected-index="{{viewState.selectedChangeIndex}}"
-          sections="[[_results]]"></gr-change-list>
+          sections="[[_results]]"
+          on-toggle-star="_handleToggleStar"></gr-change-list>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index f86c98c..9cf76da 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -241,5 +241,10 @@
     _computeUserHeaderClass(userParam) {
       return userParam === 'self' ? 'hide' : '';
     },
+
+    _handleToggleStar(e) {
+      this.$.restAPI.saveChangeStarred(e.detail.change._number,
+          e.detail.starred);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 5a56475..1b911e0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -273,6 +273,12 @@
         #commitMessageEditor {
           min-width: 0;
         }
+        .commitMessage {
+          margin-right: 0;
+        }
+        .mainChangeInfo {
+          padding-right: 0;
+        }
       }
       /* NOTE: If you update this breakpoint, also update the
       BREAKPOINT_RELATED_SMALL in the JS */
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index e217b4b..dfe5410 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -177,13 +177,15 @@
        * @param {!string} project The name of the project.
        * @param {boolean=} opt_openOnly When true, only search open changes in
        *     the project.
+       * @param {string=} opt_host The host in which to search.
        * @return {string}
        */
-      getUrlForProjectChanges(project, opt_openOnly) {
+      getUrlForProjectChanges(project, opt_openOnly, opt_host) {
         return this._getUrlFor({
           view: Gerrit.Nav.View.SEARCH,
           project,
           statuses: opt_openOnly ? ['open'] : [],
+          host: opt_host,
         });
       },
 
@@ -191,26 +193,30 @@
        * @param {string} branch The name of the branch.
        * @param {string} project The name of the project.
        * @param {string=} opt_status The status to search.
+       * @param {string=} opt_host The host in which to search.
        * @return {string}
        */
-      getUrlForBranch(branch, project, opt_status) {
+      getUrlForBranch(branch, project, opt_status, opt_host) {
         return this._getUrlFor({
           view: Gerrit.Nav.View.SEARCH,
           branch,
           project,
           statuses: opt_status ? [opt_status] : undefined,
+          host: opt_host,
         });
       },
 
       /**
        * @param {string} topic The name of the topic.
+       * @param {string=} opt_host The host in which to search.
        * @return {string}
        */
-      getUrlForTopic(topic) {
+      getUrlForTopic(topic, opt_host) {
         return this._getUrlFor({
           view: Gerrit.Nav.View.SEARCH,
           topic,
           statuses: ['open', 'merged'],
+          host: opt_host,
         });
       },
 
@@ -267,6 +273,7 @@
           patchNum: opt_patchNum,
           basePatchNum: opt_basePatchNum,
           edit: opt_isEdit,
+          host: change.internalHost || undefined,
         });
       },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index ea69fc2..edf9f5a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -457,7 +457,11 @@
         this._discardDraft();
         return;
       }
-      this._openOverlay(this.confirmDiscardOverlay);
+
+      this._openOverlay(this.confirmDiscardOverlay).then(() => {
+        this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
+            .resetFocus();
+      });
     },
 
     _handleConfirmDiscard(e) {
@@ -626,9 +630,7 @@
 
     _openOverlay(overlay) {
       Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
-      this.async(() => {
-        overlay.open();
-      }, 1);
+      return overlay.open();
     },
 
     _closeOverlay(overlay) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index b41ab23..f1ac649 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -614,7 +614,8 @@
 
       setup(() => {
         discardStub = sandbox.stub(element, '_discardDraft');
-        overlayStub = sandbox.stub(element, '_openOverlay');
+        overlayStub = sandbox.stub(element, '_openOverlay')
+            .returns(Promise.resolve());
         mockEvent = {preventDefault: sinon.stub()};
       });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 0e028d8..41d3804 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -32,7 +32,8 @@
       .gr-syntax-meta {
         color: var(--syntax-meta-color);
       }
-      .gr-syntax-keyword {
+      .gr-syntax-keyword,
+      .gr-syntax-name {
         color: var(--syntax-keyword-color);
         line-height: 1;
       }
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 14e5e6f..18c1734 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -227,6 +227,16 @@
             </span>
           </section>
           <section>
+            <span class="title">Set new changes to "work in progress" by default</span>
+            <span class="value">
+              <input
+                  id="workInProgressByDefault"
+                  type="checkbox"
+                  checked$="[[_localPrefs.work_in_progress_by_default]]"
+                  on-change="_handleWorkInProgressByDefault">
+            </span>
+          </section>
+          <section>
             <span class="title">
               Insert Signed-off-by Footer For Inline Edit Changes
             </span>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 213ab65..8eefc0a 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -24,6 +24,7 @@
     'email_strategy',
     'diff_view',
     'publish_comments_on_push',
+    'work_in_progress_by_default',
     'signed_off_by',
     'email_format',
     'size_bar_in_change_table',
@@ -291,6 +292,11 @@
           this.$.publishCommentsOnPush.checked);
     },
 
+    _handleWorkInProgressByDefault() {
+      this.set('_localPrefs.work_in_progress_by_default',
+          this.$.workInProgressByDefault.checked);
+    },
+
     _handleInsertSignedOff() {
       this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index b208ba2..f47816f 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -175,6 +175,9 @@
       assert.equal(valueOf('Publish comments on push', 'preferences')
           .firstElementChild.checked, false);
       assert.equal(valueOf(
+          'Set new changes to "work in progress" by default', 'preferences')
+          .firstElementChild.checked, false);
+      assert.equal(valueOf(
           'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
           .firstElementChild.checked, false);
 
@@ -234,6 +237,30 @@
       });
     });
 
+    test('set new changes work-in-progress', done => {
+      const newChangesWorkInProgress =
+        valueOf('Set new changes to "work in progress" by default',
+            'preferences').firstElementChild;
+      MockInteractions.tap(newChangesWorkInProgress);
+
+      assert.isFalse(element._menuChanged);
+      assert.isTrue(element._prefsChanged);
+
+      stub('gr-rest-api-interface', {
+        savePreferences(prefs) {
+          assert.equal(prefs.work_in_progress_by_default, true);
+          return Promise.resolve();
+        },
+      });
+
+      // Save the change.
+      element._handleSavePreferences().then(() => {
+        assert.isFalse(element._prefsChanged);
+        assert.isFalse(element._menuChanged);
+        done();
+      });
+    });
+
     test('diff preferences', done => {
       // Rendered with the expected preferences selected.
       assert.equal(valueOf('Context', 'diffPreferences')
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
index 70b2635..a14c652 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -17,7 +17,6 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-change-star">
@@ -36,7 +35,6 @@
           class$="[[_computeStarClass(change.starred)]]"
           icon$="[[_computeStarIcon(change.starred)]]"></iron-icon>
     </button>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-star.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 9646735..3c46d1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -20,14 +20,18 @@
   Polymer({
     is: 'gr-change-star',
 
+    /**
+     * Fired when star state is toggled.
+     *
+     * @event toggle-star
+     */
+
     properties: {
       /** @type {?} */
       change: {
         type: Object,
         notify: true,
       },
-
-      _xhrPromise: Object, // Used for testing.
     },
 
     _computeStarClass(starred) {
@@ -42,8 +46,10 @@
     toggleStar() {
       const newVal = !this.change.starred;
       this.set('change.starred', newVal);
-      this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number,
-          newVal);
+      this.dispatchEvent(new CustomEvent('toggle-star', {
+        bubbles: true,
+        detail: {change: this.change, starred: newVal},
+      }));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index f24b45c..0ca9368 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -37,9 +37,6 @@
     let element;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        saveChangeStarred() { return Promise.resolve({ok: true}); },
-      });
       element = fixture('basic');
       element.change = {
         _number: 2,
@@ -60,23 +57,21 @@
     });
 
     test('starring', done => {
-      element.set('change.starred', false);
-      MockInteractions.tap(element.$$('button'));
-
-      element._xhrPromise.then(req => {
+      element.addEventListener('toggle-star', () => {
         assert.equal(element.change.starred, true);
         done();
       });
+      element.set('change.starred', false);
+      MockInteractions.tap(element.$$('button'));
     });
 
     test('unstarring', done => {
-      element.set('change.starred', true);
-      MockInteractions.tap(element.$$('button'));
-
-      element._xhrPromise.then(req => {
+      element.addEventListener('toggle-star', () => {
         assert.equal(element.change.starred, false);
         done();
       });
+      element.set('change.starred', true);
+      MockInteractions.tap(element.$$('button'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 199a947..293ef8b 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,107 +1,107 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary")
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
 load(
     "//tools/bzl:js.bzl",
-    "vulcanize",
     "bower_component",
     "js_component",
+    "vulcanize",
 )
 
 def polygerrit_bundle(name, srcs, outs, app):
-  appName = app.split(".html")[0].split("/").pop() # eg: gr-app
+    appName = app.split(".html")[0].split("/").pop()  # eg: gr-app
 
-  closure_js_binary(
-    name = name + "_closure_bin",
-    # Known issue: Closure compilation not compatible with Polymer behaviors.
-    # See: https://github.com/google/closure-compiler/issues/2042
-    compilation_level = "WHITESPACE_ONLY",
-    defs = [
-      "--polymer_version=1",
-      "--jscomp_off=duplicate",
-      "--force_inject_library=es6_runtime",
-    ],
-    language = "ECMASCRIPT5",
-    deps = [name + "_closure_lib"],
-  )
+    closure_js_binary(
+        name = name + "_closure_bin",
+        # Known issue: Closure compilation not compatible with Polymer behaviors.
+        # See: https://github.com/google/closure-compiler/issues/2042
+        compilation_level = "WHITESPACE_ONLY",
+        defs = [
+            "--polymer_version=1",
+            "--jscomp_off=duplicate",
+            "--force_inject_library=es6_runtime",
+        ],
+        language = "ECMASCRIPT5",
+        deps = [name + "_closure_lib"],
+    )
 
-  closure_js_library(
-    name = name + "_closure_lib",
-    srcs = [appName + ".js"],
-    convention = "GOOGLE",
-    # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
-    # and remove this supression
-    suppress = [
-       "JSC_JSDOC_MISSING_TYPE_WARNING",
-       "JSC_UNNECESSARY_ESCAPE",
-       "JSC_UNUSED_LOCAL_ASSIGNMENT",
-    ],
-    deps = [
-      "//lib/polymer_externs:polymer_closure",
-      "@io_bazel_rules_closure//closure/library",
-    ],
-  )
+    closure_js_library(
+        name = name + "_closure_lib",
+        srcs = [appName + ".js"],
+        convention = "GOOGLE",
+        # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
+        # and remove this supression
+        suppress = [
+            "JSC_JSDOC_MISSING_TYPE_WARNING",
+            "JSC_UNNECESSARY_ESCAPE",
+            "JSC_UNUSED_LOCAL_ASSIGNMENT",
+        ],
+        deps = [
+            "//lib/polymer_externs:polymer_closure",
+            "@io_bazel_rules_closure//closure/library",
+        ],
+    )
 
-  vulcanize(
-    name = appName,
-    srcs = srcs,
-    app = app,
-    deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
-  )
+    vulcanize(
+        name = appName,
+        srcs = srcs,
+        app = app,
+        deps = ["//polygerrit-ui:polygerrit_components.bower_components"],
+    )
 
-  native.filegroup(
-    name = name + "_app_sources",
-    srcs = [
-      name + "_closure_bin.js",
-      appName + ".html",
-    ],
-  )
+    native.filegroup(
+        name = name + "_app_sources",
+        srcs = [
+            name + "_closure_bin.js",
+            appName + ".html",
+        ],
+    )
 
-  native.filegroup(
-    name = name + "_css_sources",
-    srcs = native.glob(["styles/**/*.css"]),
-  )
+    native.filegroup(
+        name = name + "_css_sources",
+        srcs = native.glob(["styles/**/*.css"]),
+    )
 
-  native.filegroup(
-    name = name + "_theme_sources",
-    srcs = native.glob(
-      ["styles/themes/*.html"],
-      # app-theme.html already included via an import in gr-app.html.
-      exclude = ["styles/themes/app-theme.html"],
-    ),
-  )
+    native.filegroup(
+        name = name + "_theme_sources",
+        srcs = native.glob(
+            ["styles/themes/*.html"],
+            # app-theme.html already included via an import in gr-app.html.
+            exclude = ["styles/themes/app-theme.html"],
+        ),
+    )
 
-  native.filegroup(
-    name = name + "_top_sources",
-    srcs = [
-        "favicon.ico",
-    ],
-  )
+    native.filegroup(
+        name = name + "_top_sources",
+        srcs = [
+            "favicon.ico",
+        ],
+    )
 
-  genrule2(
-    name = name,
-    srcs = [
-      name + "_app_sources",
-      name + "_css_sources",
-      name + "_theme_sources",
-      name + "_top_sources",
-      "//lib/fonts:robotofonts",
-      "//lib/js:highlightjs_files",
-      # we extract from the zip, but depend on the component for license checking.
-      "@webcomponentsjs//:zipfile",
-      "//lib/js:webcomponentsjs"
-    ],
-    outs = outs,
-    cmd = " && ".join([
-      "mkdir -p $$TMP/polygerrit_ui/{styles/themes,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
-      "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/"  + appName + ".$$ext; done",
-      "cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
-      "for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
-      "for f in $(locations "+ name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
-      "for f in $(locations "+ name + "_theme_sources); do cp $$f $$TMP/polygerrit_ui/styles/themes; done",
-      "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
-      "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
-      "cd $$TMP",
-      "find . -exec touch -t 198001010000 '{}' ';'",
-      "zip -qr $$ROOT/$@ *",
-    ]),
-  )
+    genrule2(
+        name = name,
+        srcs = [
+            name + "_app_sources",
+            name + "_css_sources",
+            name + "_theme_sources",
+            name + "_top_sources",
+            "//lib/fonts:robotofonts",
+            "//lib/js:highlightjs_files",
+            # we extract from the zip, but depend on the component for license checking.
+            "@webcomponentsjs//:zipfile",
+            "//lib/js:webcomponentsjs",
+        ],
+        outs = outs,
+        cmd = " && ".join([
+            "mkdir -p $$TMP/polygerrit_ui/{styles/themes,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
+            "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + appName + ".$$ext; done",
+            "cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
+            "for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
+            "for f in $(locations " + name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
+            "for f in $(locations " + name + "_theme_sources); do cp $$f $$TMP/polygerrit_ui/styles/themes; done",
+            "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
+            "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js",
+            "cd $$TMP",
+            "find . -exec touch -t 198001010000 '{}' ';'",
+            "zip -qr $$ROOT/$@ *",
+        ]),
+    )
diff --git a/polygerrit-ui/externs/BUILD b/polygerrit-ui/externs/BUILD
new file mode 100644
index 0000000..fab3954
--- /dev/null
+++ b/polygerrit-ui/externs/BUILD
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
+
+closure_js_library(
+    name = "plugin",
+    srcs = ["plugin.js"],
+    no_closure_library = True,
+)
diff --git a/polygerrit-ui/externs/plugin.js b/polygerrit-ui/externs/plugin.js
new file mode 100644
index 0000000..edace7d
--- /dev/null
+++ b/polygerrit-ui/externs/plugin.js
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Closure compiler externs for the Gerrit UI plugins.
+ * @externs
+ */
+
+var Gerrit = {};
+
+/**
+ * @param {!Function} callback
+ */
+Gerrit.install = function(callback) {};
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl
index 263b465..97d68d6 100644
--- a/tools/bzl/asciidoc.bzl
+++ b/tools/bzl/asciidoc.bzl
@@ -1,40 +1,43 @@
 def documentation_attributes():
-  return [
-    "toc2",
-    'newline="\\n"',
-    'asterisk="&#42;"',
-    'plus="&#43;"',
-    'caret="&#94;"',
-    'startsb="&#91;"',
-    'endsb="&#93;"',
-    'tilde="&#126;"',
-    "last-update-label!",
-    "source-highlighter=prettify",
-    "stylesheet=DEFAULT",
-    "linkcss=true",
-    "prettifydir=.",
-    # Just a placeholder, will be filled in asciidoctor java binary:
-    "revnumber=%s",
-  ]
+    return [
+        "toc2",
+        'newline="\\n"',
+        'asterisk="&#42;"',
+        'plus="&#43;"',
+        'caret="&#94;"',
+        'startsb="&#91;"',
+        'endsb="&#93;"',
+        'tilde="&#126;"',
+        "last-update-label!",
+        "source-highlighter=prettify",
+        "stylesheet=DEFAULT",
+        "linkcss=true",
+        "prettifydir=.",
+        # Just a placeholder, will be filled in asciidoctor java binary:
+        "revnumber=%s",
+    ]
 
 def _replace_macros_impl(ctx):
-  cmd = [
-    ctx.file._exe.path,
-    '--suffix', ctx.attr.suffix,
-    "-s", ctx.file.src.path,
-    "-o", ctx.outputs.out.path,
-  ]
-  if ctx.attr.searchbox:
-    cmd.append('--searchbox')
-  else:
-    cmd.append('--no-searchbox')
-  ctx.actions.run_shell(
-    inputs = [ctx.file._exe, ctx.file.src],
-    outputs = [ctx.outputs.out],
-    command = cmd,
-    use_default_shell_env = True,
-    progress_message = "Replacing macros in %s" % ctx.file.src.short_path,
-  )
+    cmd = [
+        ctx.file._exe.path,
+        "--suffix",
+        ctx.attr.suffix,
+        "-s",
+        ctx.file.src.path,
+        "-o",
+        ctx.outputs.out.path,
+    ]
+    if ctx.attr.searchbox:
+        cmd.append("--searchbox")
+    else:
+        cmd.append("--no-searchbox")
+    ctx.actions.run_shell(
+        inputs = [ctx.file._exe, ctx.file.src],
+        outputs = [ctx.outputs.out],
+        command = cmd,
+        use_default_shell_env = True,
+        progress_message = "Replacing macros in %s" % ctx.file.src.short_path,
+    )
 
 _replace_macros = rule(
     attrs = {
@@ -54,52 +57,55 @@
 )
 
 def _generate_asciidoc_args(ctx):
-  args = []
-  if ctx.attr.backend:
-    args.extend(["-b", ctx.attr.backend])
-  revnumber = False
-  for attribute in ctx.attr.attributes:
-    if attribute.startswith("revnumber="):
-      revnumber = True
-    else:
-      args.extend(["-a", attribute])
-  if revnumber:
-    args.extend([
-      "--revnumber-file", ctx.file.version.path,
-    ])
-  for src in ctx.files.srcs:
-    args.append(src.path)
-  return args
+    args = []
+    if ctx.attr.backend:
+        args.extend(["-b", ctx.attr.backend])
+    revnumber = False
+    for attribute in ctx.attr.attributes:
+        if attribute.startswith("revnumber="):
+            revnumber = True
+        else:
+            args.extend(["-a", attribute])
+    if revnumber:
+        args.extend([
+            "--revnumber-file",
+            ctx.file.version.path,
+        ])
+    for src in ctx.files.srcs:
+        args.append(src.path)
+    return args
 
 def _invoke_replace_macros(name, src, suffix, searchbox):
-  fn = src
-  if fn.startswith(":"):
-    fn = src[1:]
+    fn = src
+    if fn.startswith(":"):
+        fn = src[1:]
 
-  _replace_macros(
-    name = "macros_%s_%s" % (name, fn),
-    src = src,
-    out = fn + suffix,
-    suffix = suffix,
-    searchbox = searchbox,
-  )
+    _replace_macros(
+        name = "macros_%s_%s" % (name, fn),
+        src = src,
+        out = fn + suffix,
+        suffix = suffix,
+        searchbox = searchbox,
+    )
 
-  return ":" + fn + suffix, fn.replace(".txt", ".html")
+    return ":" + fn + suffix, fn.replace(".txt", ".html")
 
 def _asciidoc_impl(ctx):
-  args = [
-    "--bazel",
-    "--in-ext", ".txt" + ctx.attr.suffix,
-    "--out-ext", ".html",
-  ]
-  args.extend(_generate_asciidoc_args(ctx))
-  ctx.actions.run(
-    inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
-    outputs = ctx.outputs.outs,
-    executable = ctx.executable._exe,
-    arguments = args,
-    progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
-  )
+    args = [
+        "--bazel",
+        "--in-ext",
+        ".txt" + ctx.attr.suffix,
+        "--out-ext",
+        ".html",
+    ]
+    args.extend(_generate_asciidoc_args(ctx))
+    ctx.actions.run(
+        inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
+        outputs = ctx.outputs.outs,
+        executable = ctx.executable._exe,
+        arguments = args,
+        progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+    )
 
 _asciidoc_attrs = {
     "_exe": attr.label(
@@ -129,82 +135,85 @@
 )
 
 def _genasciidoc_htmlonly(
-    name,
-    srcs = [],
-    attributes = [],
-    backend = None,
-    searchbox = True,
-    **kwargs):
-  SUFFIX = "." + name + "_macros"
-  new_srcs = []
-  outs = ["asciidoctor.css"]
+        name,
+        srcs = [],
+        attributes = [],
+        backend = None,
+        searchbox = True,
+        **kwargs):
+    SUFFIX = "." + name + "_macros"
+    new_srcs = []
+    outs = ["asciidoctor.css"]
 
-  for src in srcs:
-    new_src, html_name = _invoke_replace_macros(name, src, SUFFIX, searchbox)
-    new_srcs.append(new_src)
-    outs.append(html_name)
+    for src in srcs:
+        new_src, html_name = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+        new_srcs.append(new_src)
+        outs.append(html_name)
 
-  _asciidoc(
-    name = name + "_gen",
-    srcs = new_srcs,
-    suffix = SUFFIX,
-    backend = backend,
-    attributes = attributes,
-    outs = outs,
-  )
-
-  native.filegroup(
-    name = name,
-    data = outs,
-    **kwargs
-  )
-
-def genasciidoc(
-    name,
-    srcs = [],
-    attributes = [],
-    backend = None,
-    searchbox = True,
-    resources = True,
-    **kwargs):
-  SUFFIX = "_htmlonly"
-
-  _genasciidoc_htmlonly(
-    name = name + SUFFIX if resources else name,
-    srcs = srcs,
-    attributes = attributes,
-    backend = backend,
-    searchbox = searchbox,
-    **kwargs
-  )
-
-  if resources:
-    htmlonly = ":" + name + SUFFIX
-    native.filegroup(
-      name = name,
-      srcs = [
-        htmlonly,
-        "//Documentation:resources",
-      ],
-      **kwargs
+    _asciidoc(
+        name = name + "_gen",
+        srcs = new_srcs,
+        suffix = SUFFIX,
+        backend = backend,
+        attributes = attributes,
+        outs = outs,
     )
 
+    native.filegroup(
+        name = name,
+        data = outs,
+        **kwargs
+    )
+
+def genasciidoc(
+        name,
+        srcs = [],
+        attributes = [],
+        backend = None,
+        searchbox = True,
+        resources = True,
+        **kwargs):
+    SUFFIX = "_htmlonly"
+
+    _genasciidoc_htmlonly(
+        name = name + SUFFIX if resources else name,
+        srcs = srcs,
+        attributes = attributes,
+        backend = backend,
+        searchbox = searchbox,
+        **kwargs
+    )
+
+    if resources:
+        htmlonly = ":" + name + SUFFIX
+        native.filegroup(
+            name = name,
+            srcs = [
+                htmlonly,
+                "//Documentation:resources",
+            ],
+            **kwargs
+        )
+
 def _asciidoc_html_zip_impl(ctx):
-  args = [
-    "--mktmp",
-    "-z", ctx.outputs.out.path,
-    "--in-ext", ".txt" + ctx.attr.suffix,
-    "--out-ext", ".html",
-  ]
-  args.extend(_generate_asciidoc_args(ctx))
-  ctx.actions.run(
-    inputs = ctx.files.srcs + [ctx.file.version],
-    outputs = [ctx.outputs.out],
-    tools = [ctx.executable._exe],
-    executable = ctx.executable._exe,
-    arguments = args,
-    progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
-  )
+    args = [
+        "--mktmp",
+        "-z",
+        ctx.outputs.out.path,
+        "--in-ext",
+        ".txt" + ctx.attr.suffix,
+        "--out-ext",
+        ".html",
+    ]
+    args.extend(_generate_asciidoc_args(ctx))
+    ctx.actions.run(
+        inputs = ctx.files.srcs + [ctx.file.version],
+        outputs = [ctx.outputs.out],
+        tools = [ctx.executable._exe],
+        executable = ctx.executable._exe,
+        arguments = args,
+        progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+    )
 
 _asciidoc_html_zip = rule(
     attrs = _asciidoc_attrs,
@@ -215,53 +224,54 @@
 )
 
 def _genasciidoc_htmlonly_zip(
-    name,
-    srcs = [],
-    attributes = [],
-    backend = None,
-    searchbox = True,
-    **kwargs):
-  SUFFIX = "." + name + "_expn"
-  new_srcs = []
+        name,
+        srcs = [],
+        attributes = [],
+        backend = None,
+        searchbox = True,
+        **kwargs):
+    SUFFIX = "." + name + "_expn"
+    new_srcs = []
 
-  for src in srcs:
-    new_src, _ = _invoke_replace_macros(name, src, SUFFIX, searchbox)
-    new_srcs.append(new_src)
+    for src in srcs:
+        new_src, _ = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+        new_srcs.append(new_src)
 
-  _asciidoc_html_zip(
-    name = name,
-    srcs = new_srcs,
-    suffix = SUFFIX,
-    backend = backend,
-    attributes = attributes,
-  )
+    _asciidoc_html_zip(
+        name = name,
+        srcs = new_srcs,
+        suffix = SUFFIX,
+        backend = backend,
+        attributes = attributes,
+    )
 
 def _asciidoc_zip_impl(ctx):
-  tmpdir = ctx.outputs.out.path + "_tmpdir"
-  cmd = [
-    "p=$PWD",
-    "rm -rf %s" % tmpdir,
-    "mkdir -p %s/%s/" % (tmpdir, ctx.attr.directory),
-    "unzip -q %s -d %s/%s/" % (ctx.file.src.path, tmpdir, ctx.attr.directory),
-  ]
-  for r in ctx.files.resources:
-    if r.path == r.short_path:
-      cmd.append("tar -cf- %s | tar -C %s -xf-" % (r.short_path, tmpdir))
-    else:
-      parent = r.path[:-len(r.short_path)]
-      cmd.append(
-        "tar -C %s -cf- %s | tar -C %s -xf-" % (parent, r.short_path, tmpdir))
-  cmd.extend([
-    "cd %s" % tmpdir,
-    "zip -qr $p/%s *" % ctx.outputs.out.path,
-  ])
-  ctx.actions.run_shell(
-    inputs = [ctx.file.src] + ctx.files.resources,
-    outputs = [ctx.outputs.out],
-    command = " && ".join(cmd),
-    progress_message =
-        "Generating asciidoctor zip file %s" % ctx.outputs.out.short_path,
-  )
+    tmpdir = ctx.outputs.out.path + "_tmpdir"
+    cmd = [
+        "p=$PWD",
+        "rm -rf %s" % tmpdir,
+        "mkdir -p %s/%s/" % (tmpdir, ctx.attr.directory),
+        "unzip -q %s -d %s/%s/" % (ctx.file.src.path, tmpdir, ctx.attr.directory),
+    ]
+    for r in ctx.files.resources:
+        if r.path == r.short_path:
+            cmd.append("tar -cf- %s | tar -C %s -xf-" % (r.short_path, tmpdir))
+        else:
+            parent = r.path[:-len(r.short_path)]
+            cmd.append(
+                "tar -C %s -cf- %s | tar -C %s -xf-" % (parent, r.short_path, tmpdir),
+            )
+    cmd.extend([
+        "cd %s" % tmpdir,
+        "zip -qr $p/%s *" % ctx.outputs.out.path,
+    ])
+    ctx.actions.run_shell(
+        inputs = [ctx.file.src] + ctx.files.resources,
+        outputs = [ctx.outputs.out],
+        command = " && ".join(cmd),
+        progress_message =
+            "Generating asciidoctor zip file %s" % ctx.outputs.out.short_path,
+    )
 
 _asciidoc_zip = rule(
     attrs = {
@@ -282,30 +292,30 @@
 )
 
 def genasciidoc_zip(
-    name,
-    srcs = [],
-    attributes = [],
-    directory = None,
-    backend = None,
-    searchbox = True,
-    resources = True,
-    **kwargs):
-  SUFFIX = "_htmlonly"
+        name,
+        srcs = [],
+        attributes = [],
+        directory = None,
+        backend = None,
+        searchbox = True,
+        resources = True,
+        **kwargs):
+    SUFFIX = "_htmlonly"
 
-  _genasciidoc_htmlonly_zip(
-    name = name + SUFFIX if resources else name,
-    srcs = srcs,
-    attributes = attributes,
-    backend = backend,
-    searchbox = searchbox,
-    **kwargs
-  )
-
-  if resources:
-    htmlonly = ":" + name + SUFFIX
-    _asciidoc_zip(
-      name = name,
-      src = htmlonly,
-      resources = ["//Documentation:resources"],
-      directory = directory,
+    _genasciidoc_htmlonly_zip(
+        name = name + SUFFIX if resources else name,
+        srcs = srcs,
+        attributes = attributes,
+        backend = backend,
+        searchbox = searchbox,
+        **kwargs
     )
+
+    if resources:
+        htmlonly = ":" + name + SUFFIX
+        _asciidoc_zip(
+            name = name,
+            src = htmlonly,
+            resources = ["//Documentation:resources"],
+            directory = directory,
+        )
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index 9448ed1..afdd907 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,15 +1,17 @@
 def _classpath_collector(ctx):
     all = depset()
     for d in ctx.attr.deps:
-        if hasattr(d, 'java'):
+        if hasattr(d, "java"):
             all += d.java.transitive_runtime_deps
             all += d.java.compilation_info.runtime_classpath
-        elif hasattr(d, 'files'):
+        elif hasattr(d, "files"):
             all += d.files
 
     as_strs = [c.path for c in all]
-    ctx.file_action(output= ctx.outputs.runtime,
-                    content="\n".join(sorted(as_strs)))
+    ctx.file_action(
+        output = ctx.outputs.runtime,
+        content = "\n".join(sorted(as_strs)),
+    )
 
 classpath_collector = rule(
     attrs = {
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl
index 563a9ef..3113022 100644
--- a/tools/bzl/genrule2.bzl
+++ b/tools/bzl/genrule2.bzl
@@ -17,11 +17,12 @@
 #   expose TMP shell variable
 
 def genrule2(cmd, **kwargs):
-  cmd = ' && '.join([
-    'ROOT=$$PWD',
-    'TMP=$$(mktemp -d || mktemp -d -t bazel-tmp)',
-    '(' + cmd + ')',
-  ])
-  native.genrule(
-    cmd = cmd,
-    **kwargs)
+    cmd = " && ".join([
+        "ROOT=$$PWD",
+        "TMP=$$(mktemp -d || mktemp -d -t bazel-tmp)",
+        "(" + cmd + ")",
+    ])
+    native.genrule(
+        cmd = cmd,
+        **kwargs
+    )
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index 71ba750..2adb7dd 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -90,117 +90,122 @@
 </module>
 """
 
-def gwt_module(gwt_xml=None, resources=[], srcs=[], **kwargs):
-  if gwt_xml:
-    resources = resources + [gwt_xml]
+def gwt_module(gwt_xml = None, resources = [], srcs = [], **kwargs):
+    if gwt_xml:
+        resources = resources + [gwt_xml]
 
-  java_library2(
-    srcs = srcs,
-    resources = resources,
-    **kwargs)
+    java_library2(
+        srcs = srcs,
+        resources = resources,
+        **kwargs
+    )
 
 def _gwt_user_agent_module(ctx):
-  """Generate user agent specific GWT module."""
-  if not ctx.attr.user_agent:
-    return None
+    """Generate user agent specific GWT module."""
+    if not ctx.attr.user_agent:
+        return None
 
-  ua = ctx.attr.user_agent
-  impl = ua
-  if ua in ALIASES:
-    impl = ALIASES[ua]
+    ua = ctx.attr.user_agent
+    impl = ua
+    if ua in ALIASES:
+        impl = ALIASES[ua]
 
-  # intermediate artifact: user agent speific GWT xml file
-  gwt_user_agent_xml = ctx.new_file(ctx.label.name + "_gwt.xml")
-  ctx.file_action(output = gwt_user_agent_xml,
-                  content=USER_AGENT_XML % (MODULE, impl))
+    # intermediate artifact: user agent speific GWT xml file
+    gwt_user_agent_xml = ctx.new_file(ctx.label.name + "_gwt.xml")
+    ctx.file_action(
+        output = gwt_user_agent_xml,
+        content = USER_AGENT_XML % (MODULE, impl),
+    )
 
-  # intermediate artifact: user agent specific zip with GWT module
-  gwt_user_agent_zip = ctx.new_file(ctx.label.name + "_gwt.zip")
-  gwt = '%s_%s.gwt.xml' % (MODULE.replace('.', '/'), ua)
-  dir = gwt_user_agent_zip.path + ".dir"
-  cmd = " && ".join([
-    "p=$PWD",
-    "mkdir -p %s" % dir,
-    "cd %s" % dir,
-    "mkdir -p $(dirname %s)" % gwt,
-    "cp $p/%s %s" % (gwt_user_agent_xml.path, gwt),
-    "$p/%s cC $p/%s $(find . | sed 's|^./||')" % (ctx.executable._zip.path, gwt_user_agent_zip.path)
-  ])
-  ctx.actions.run_shell(
-    inputs = [gwt_user_agent_xml] + ctx.files._zip,
-    outputs = [gwt_user_agent_zip],
-    command = cmd,
-    mnemonic = "GenerateUserAgentGWTModule")
+    # intermediate artifact: user agent specific zip with GWT module
+    gwt_user_agent_zip = ctx.new_file(ctx.label.name + "_gwt.zip")
+    gwt = "%s_%s.gwt.xml" % (MODULE.replace(".", "/"), ua)
+    dir = gwt_user_agent_zip.path + ".dir"
+    cmd = " && ".join([
+        "p=$PWD",
+        "mkdir -p %s" % dir,
+        "cd %s" % dir,
+        "mkdir -p $(dirname %s)" % gwt,
+        "cp $p/%s %s" % (gwt_user_agent_xml.path, gwt),
+        "$p/%s cC $p/%s $(find . | sed 's|^./||')" % (ctx.executable._zip.path, gwt_user_agent_zip.path),
+    ])
+    ctx.actions.run_shell(
+        inputs = [gwt_user_agent_xml] + ctx.files._zip,
+        outputs = [gwt_user_agent_zip],
+        command = cmd,
+        mnemonic = "GenerateUserAgentGWTModule",
+    )
 
-  return struct(
-    zip=gwt_user_agent_zip,
-    module=MODULE + '_' + ua
-  )
+    return struct(
+        zip = gwt_user_agent_zip,
+        module = MODULE + "_" + ua,
+    )
 
 def _gwt_binary_impl(ctx):
-  module = ctx.attr.module[0]
-  output_zip = ctx.outputs.output
-  output_dir = output_zip.path + '.gwt_output'
-  deploy_dir = output_zip.path + '.gwt_deploy'
+    module = ctx.attr.module[0]
+    output_zip = ctx.outputs.output
+    output_dir = output_zip.path + ".gwt_output"
+    deploy_dir = output_zip.path + ".gwt_deploy"
 
-  deps = _get_transitive_closure(ctx)
+    deps = _get_transitive_closure(ctx)
 
-  paths = []
-  for dep in deps:
-    paths.append(dep.path)
+    paths = []
+    for dep in deps:
+        paths.append(dep.path)
 
-  gwt_user_agent_modules = []
-  ua = _gwt_user_agent_module(ctx)
-  if ua:
-    paths.append(ua.zip.path)
-    gwt_user_agent_modules.append(ua.zip)
-    module = ua.module
+    gwt_user_agent_modules = []
+    ua = _gwt_user_agent_module(ctx)
+    if ua:
+        paths.append(ua.zip.path)
+        gwt_user_agent_modules.append(ua.zip)
+        module = ua.module
 
-  cmd = "%s %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
-    ctx.attr._jdk[java_common.JavaRuntimeInfo].java_executable_exec_path,
-    " ".join(ctx.attr.jvm_args),
-    ":".join(paths),
-    GWT_COMPILER,
-    output_dir,
-    deploy_dir,
-  )
-  # TODO(davido): clean up command concatenation
-  cmd += " ".join([
-    "-style %s" % ctx.attr.style,
-    "-optimize %s" % ctx.attr.optimize,
-    "-strict",
-    " ".join(ctx.attr.compiler_args),
-    module + "\n",
-    "rm -rf %s/gwt-unitCache\n" % output_dir,
-    "root=`pwd`\n",
-    "cd %s; $root/%s Cc ../%s $(find .)\n" % (
-      output_dir,
-      ctx.executable._zip.path,
-      output_zip.basename,
+    cmd = "%s %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
+        ctx.attr._jdk[java_common.JavaRuntimeInfo].java_executable_exec_path,
+        " ".join(ctx.attr.jvm_args),
+        ":".join(paths),
+        GWT_COMPILER,
+        output_dir,
+        deploy_dir,
     )
-  ])
 
-  ctx.actions.run_shell(
-    inputs = list(deps) + gwt_user_agent_modules,
-    outputs = [output_zip],
-    tools = ctx.files._jdk + ctx.files._zip,
-    mnemonic = "GwtBinary",
-    progress_message = "GWT compiling " + output_zip.short_path,
-    command = "set -e\n" + cmd,
-  )
+    # TODO(davido): clean up command concatenation
+    cmd += " ".join([
+        "-style %s" % ctx.attr.style,
+        "-optimize %s" % ctx.attr.optimize,
+        "-strict",
+        " ".join(ctx.attr.compiler_args),
+        module + "\n",
+        "rm -rf %s/gwt-unitCache\n" % output_dir,
+        "root=`pwd`\n",
+        "cd %s; $root/%s Cc ../%s $(find .)\n" % (
+            output_dir,
+            ctx.executable._zip.path,
+            output_zip.basename,
+        ),
+    ])
+
+    ctx.actions.run_shell(
+        inputs = list(deps) + gwt_user_agent_modules,
+        outputs = [output_zip],
+        tools = ctx.files._jdk + ctx.files._zip,
+        mnemonic = "GwtBinary",
+        progress_message = "GWT compiling " + output_zip.short_path,
+        command = "set -e\n" + cmd,
+    )
 
 def _get_transitive_closure(ctx):
-  deps = depset()
-  for dep in ctx.attr.module_deps:
-    deps += dep.java.transitive_runtime_deps
-    deps += dep.java.transitive_source_jars
-  for dep in ctx.attr.deps:
-    if hasattr(dep, 'java'):
-      deps += dep.java.transitive_runtime_deps
-    elif hasattr(dep, 'files'):
-      deps += dep.files
+    deps = depset()
+    for dep in ctx.attr.module_deps:
+        deps += dep.java.transitive_runtime_deps
+        deps += dep.java.transitive_source_jars
+    for dep in ctx.attr.deps:
+        if hasattr(dep, "java"):
+            deps += dep.java.transitive_runtime_deps
+        elif hasattr(dep, "files"):
+            deps += dep.files
 
-  return deps
+    return deps
 
 gwt_binary = rule(
     attrs = {
@@ -230,77 +235,78 @@
 )
 
 def gwt_genrule(suffix = ""):
-  dbg = 'ui_dbg' + suffix
-  opt = 'ui_opt' + suffix
-  module_dep = ':ui_module' + suffix
-  args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
+    dbg = "ui_dbg" + suffix
+    opt = "ui_opt" + suffix
+    module_dep = ":ui_module" + suffix
+    args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
 
-  genrule2(
-    name = 'ui_optdbg' + suffix,
-    srcs = [
-      ':' + dbg,
-      ':' + opt,
-     ],
-    cmd = 'cd $$TMP;' +
-      'unzip -q $$ROOT/$(location :%s);' % dbg +
-      'mv' +
-      ' gerrit_ui/gerrit_ui.nocache.js' +
-      ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
-      'unzip -qo $$ROOT/$(location :%s);' % opt +
-      'mkdir -p $$(dirname $@);' +
-      'zip -qrD $$ROOT/$@ .',
-    outs = ['ui_optdbg' + suffix + '.zip'],
-    visibility = ['//visibility:public'],
-   )
+    genrule2(
+        name = "ui_optdbg" + suffix,
+        srcs = [
+            ":" + dbg,
+            ":" + opt,
+        ],
+        cmd = "cd $$TMP;" +
+              "unzip -q $$ROOT/$(location :%s);" % dbg +
+              "mv" +
+              " gerrit_ui/gerrit_ui.nocache.js" +
+              " gerrit_ui/dbg_gerrit_ui.nocache.js;" +
+              "unzip -qo $$ROOT/$(location :%s);" % opt +
+              "mkdir -p $$(dirname $@);" +
+              "zip -qrD $$ROOT/$@ .",
+        outs = ["ui_optdbg" + suffix + ".zip"],
+        visibility = ["//visibility:public"],
+    )
 
-  gwt_binary(
-    name = opt,
-    module = [MODULE],
-    module_deps = [module_dep],
-    deps = DEPS,
-    compiler_args = args,
-    jvm_args = GWT_JVM_ARGS,
-  )
+    gwt_binary(
+        name = opt,
+        module = [MODULE],
+        module_deps = [module_dep],
+        deps = DEPS,
+        compiler_args = args,
+        jvm_args = GWT_JVM_ARGS,
+    )
 
-  gwt_binary(
-    name = dbg,
-    style = 'PRETTY',
-    optimize = "0",
-    module_deps = [module_dep],
-    deps = DEPS,
-    compiler_args = GWT_COMPILER_ARGS,
-    jvm_args = GWT_JVM_ARGS,
-  )
+    gwt_binary(
+        name = dbg,
+        style = "PRETTY",
+        optimize = "0",
+        module_deps = [module_dep],
+        deps = DEPS,
+        compiler_args = GWT_COMPILER_ARGS,
+        jvm_args = GWT_JVM_ARGS,
+    )
 
 def gen_ui_module(name, suffix = ""):
-  gwt_module(
-    name = name + suffix,
-    srcs = native.glob(['src/main/java/**/*.java']),
-    gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
-    resources = native.glob(
-        ['src/main/java/**/*'],
-        exclude = ['src/main/java/**/*.java'] +
-        ['src/main/java/%s.gwt.xml' % MODULE.replace('.', '/')]),
-    deps = [
-      '//gerrit-gwtui-common:diffy_logo',
-      '//gerrit-gwtui-common:client',
-      '//java/com/google/gwtexpui/css',
-      '//lib/codemirror:codemirror' + suffix,
-      '//lib/gwt:user',
-    ],
-    visibility = ['//visibility:public'],
-  )
+    gwt_module(
+        name = name + suffix,
+        srcs = native.glob(["src/main/java/**/*.java"]),
+        gwt_xml = "src/main/java/%s.gwt.xml" % MODULE.replace(".", "/"),
+        resources = native.glob(
+            ["src/main/java/**/*"],
+            exclude = ["src/main/java/**/*.java"] +
+                      ["src/main/java/%s.gwt.xml" % MODULE.replace(".", "/")],
+        ),
+        deps = [
+            "//gerrit-gwtui-common:diffy_logo",
+            "//gerrit-gwtui-common:client",
+            "//java/com/google/gwtexpui/css",
+            "//lib/codemirror:codemirror" + suffix,
+            "//lib/gwt:user",
+        ],
+        visibility = ["//visibility:public"],
+    )
 
 def gwt_user_agent_permutations():
-  for ua in BROWSERS:
-    gwt_binary(
-      name = "ui_%s" % ua,
-      user_agent = ua,
-      style = 'PRETTY',
-      optimize = "0",
-      module = [MODULE],
-      module_deps = [':ui_module'],
-      deps = DEPS,
-      compiler_args = GWT_COMPILER_ARGS,
-      jvm_args = GWT_JVM_ARGS,
-    )
+    for ua in BROWSERS:
+        gwt_binary(
+            name = "ui_%s" % ua,
+            user_agent = ua,
+            style = "PRETTY",
+            optimize = "0",
+            module = [MODULE],
+            module_deps = [":ui_module"],
+            deps = DEPS,
+            compiler_args = GWT_COMPILER_ARGS,
+            jvm_args = GWT_JVM_ARGS,
+        )
diff --git a/tools/bzl/java.bzl b/tools/bzl/java.bzl
index 5fca724..7c41fbe 100644
--- a/tools/bzl/java.bzl
+++ b/tools/bzl/java.bzl
@@ -15,11 +15,12 @@
 # Syntactic sugar for native java_library() rule:
 #   accept exported_deps attributes
 
-def java_library2(deps=[], exported_deps=[], exports=[], **kwargs):
-  if exported_deps:
-    deps = deps + exported_deps
-    exports = exports + exported_deps
-  native.java_library(
-    deps = deps,
-    exports = exports,
-    **kwargs)
+def java_library2(deps = [], exported_deps = [], exports = [], **kwargs):
+    if exported_deps:
+        deps = deps + exported_deps
+        exports = exports + exported_deps
+    native.java_library(
+        deps = deps,
+        exports = exports,
+        **kwargs
+    )
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index f49c881..34ae9d2 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -15,49 +15,51 @@
 # Javadoc rule.
 
 def _impl(ctx):
-  zip_output = ctx.outputs.zip
+    zip_output = ctx.outputs.zip
 
-  transitive_jar_set = depset()
-  source_jars = depset()
-  for l in ctx.attr.libs:
-    source_jars += l.java.source_jars
-    transitive_jar_set += l.java.transitive_deps
+    transitive_jar_set = depset()
+    source_jars = depset()
+    for l in ctx.attr.libs:
+        source_jars += l.java.source_jars
+        transitive_jar_set += l.java.transitive_deps
 
-  transitive_jar_paths = [j.path for j in transitive_jar_set]
-  dir = ctx.outputs.zip.path + ".dir"
-  source = ctx.outputs.zip.path + ".source"
-  external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs
-  cmd = [
-      "TZ=UTC",
-      "export TZ",
-      "rm -rf %s" % source,
-      "mkdir %s" % source,
-      " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]),
-      "rm -rf %s" % dir,
-      "mkdir %s" % dir,
-      " ".join([
-        ctx.file._javadoc.path,
-        "-Xdoclint:-missing",
-        "-protected",
-        "-encoding UTF-8",
-        "-charset UTF-8",
-        "-notimestamp",
-        "-quiet",
-        "-windowtitle '%s'" % ctx.attr.title,
-        " ".join(['-link %s' % url for url in external_docs]),
-        "-sourcepath %s" % source,
-        "-subpackages ",
-        ":".join(ctx.attr.pkgs),
-        " -classpath ",
-        ":".join(transitive_jar_paths),
-        "-d %s" % dir]),
-    "find %s -exec touch -t 198001010000 '{}' ';'" % dir,
-    "(cd %s && zip -Xqr ../%s *)" % (dir, ctx.outputs.zip.basename),
-  ]
-  ctx.actions.run_shell(
-      inputs = list(transitive_jar_set) + list(source_jars) + ctx.files._jdk,
-      outputs = [zip_output],
-      command = " && ".join(cmd))
+    transitive_jar_paths = [j.path for j in transitive_jar_set]
+    dir = ctx.outputs.zip.path + ".dir"
+    source = ctx.outputs.zip.path + ".source"
+    external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs
+    cmd = [
+        "TZ=UTC",
+        "export TZ",
+        "rm -rf %s" % source,
+        "mkdir %s" % source,
+        " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]),
+        "rm -rf %s" % dir,
+        "mkdir %s" % dir,
+        " ".join([
+            ctx.file._javadoc.path,
+            "-Xdoclint:-missing",
+            "-protected",
+            "-encoding UTF-8",
+            "-charset UTF-8",
+            "-notimestamp",
+            "-quiet",
+            "-windowtitle '%s'" % ctx.attr.title,
+            " ".join(["-link %s" % url for url in external_docs]),
+            "-sourcepath %s" % source,
+            "-subpackages ",
+            ":".join(ctx.attr.pkgs),
+            " -classpath ",
+            ":".join(transitive_jar_paths),
+            "-d %s" % dir,
+        ]),
+        "find %s -exec touch -t 198001010000 '{}' ';'" % dir,
+        "(cd %s && zip -Xqr ../%s *)" % (dir, ctx.outputs.zip.basename),
+    ]
+    ctx.actions.run_shell(
+        inputs = list(transitive_jar_set) + list(source_jars) + ctx.files._jdk,
+        outputs = [zip_output],
+        command = " && ".join(cmd),
+    )
 
 java_doc = rule(
     attrs = {
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 2796f64..6590cd6 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -2,37 +2,38 @@
 
 GERRIT = "GERRIT:"
 
-load("//lib/js:npm.bzl", "NPM_VERSIONS", "NPM_SHA1S")
+load("//lib/js:npm.bzl", "NPM_SHA1S", "NPM_VERSIONS")
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
 
 def _npm_tarball(name):
-  return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
+    return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
 
 def _npm_binary_impl(ctx):
-  """rule to download a NPM archive."""
-  name = ctx.name
-  version= NPM_VERSIONS[name]
-  sha1 = NPM_SHA1S[name]
+    """rule to download a NPM archive."""
+    name = ctx.name
+    version = NPM_VERSIONS[name]
+    sha1 = NPM_SHA1S[name]
 
-  dir = '%s-%s' % (name, version)
-  filename = '%s.tgz' % dir
-  base =  '%s@%s.npm_binary.tgz' % (name, version)
-  dest = ctx.path(base)
-  repository = ctx.attr.repository
-  if repository == GERRIT:
-    url = 'http://gerrit-maven.storage.googleapis.com/npm-packages/%s' % filename
-  elif repository == NPMJS:
-    url = 'http://registry.npmjs.org/%s/-/%s' % (name, filename)
-  else:
-    fail('repository %s not in {%s,%s}' % (repository, GERRIT, NPMJS))
+    dir = "%s-%s" % (name, version)
+    filename = "%s.tgz" % dir
+    base = "%s@%s.npm_binary.tgz" % (name, version)
+    dest = ctx.path(base)
+    repository = ctx.attr.repository
+    if repository == GERRIT:
+        url = "http://gerrit-maven.storage.googleapis.com/npm-packages/%s" % filename
+    elif repository == NPMJS:
+        url = "http://registry.npmjs.org/%s/-/%s" % (name, filename)
+    else:
+        fail("repository %s not in {%s,%s}" % (repository, GERRIT, NPMJS))
 
-  python = ctx.which("python")
-  script = ctx.path(ctx.attr._download_script)
+    python = ctx.which("python")
+    script = ctx.path(ctx.attr._download_script)
 
-  args = [python, script, "-o", dest, "-u", url, "-v", sha1]
-  out = ctx.execute(args)
-  if out.return_code:
-    fail("failed %s: %s" % (args, out.stderr))
-  ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
+    args = [python, script, "-o", dest, "-u", url, "-v", sha1]
+    out = ctx.execute(args)
+    if out.return_code:
+        fail("failed %s: %s" % (args, out.stderr))
+    ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
 
 npm_binary = repository_rule(
     attrs = {
@@ -46,64 +47,75 @@
 
 # for use in repo rules.
 def _run_npm_binary_str(ctx, tarball, args):
-  python_bin = ctx.which("python")
-  return " ".join([
-    python_bin,
-    ctx.path(ctx.attr._run_npm),
-    ctx.path(tarball)] + args)
+    python_bin = ctx.which("python")
+    return " ".join([
+        python_bin,
+        ctx.path(ctx.attr._run_npm),
+        ctx.path(tarball),
+    ] + args)
 
 def _bower_archive(ctx):
-  """Download a bower package."""
-  download_name = '%s__download_bower.zip' % ctx.name
-  renamed_name = '%s__renamed.zip' % ctx.name
-  version_name = '%s__version.json' % ctx.name
+    """Download a bower package."""
+    download_name = "%s__download_bower.zip" % ctx.name
+    renamed_name = "%s__renamed.zip" % ctx.name
+    version_name = "%s__version.json" % ctx.name
 
-  cmd = [
-      ctx.which("python"),
-      ctx.path(ctx.attr._download_bower),
-      '-b', '%s' % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
-      '-n', ctx.name,
-      '-p', ctx.attr.package,
-      '-v', ctx.attr.version,
-      '-s', ctx.attr.sha1,
-      '-o', download_name,
+    cmd = [
+        ctx.which("python"),
+        ctx.path(ctx.attr._download_bower),
+        "-b",
+        "%s" % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
+        "-n",
+        ctx.name,
+        "-p",
+        ctx.attr.package,
+        "-v",
+        ctx.attr.version,
+        "-s",
+        ctx.attr.sha1,
+        "-o",
+        download_name,
     ]
 
-  out = ctx.execute(cmd)
-  if out.return_code:
-    fail("failed %s: %s" % (" ".join(cmd), out.stderr))
+    out = ctx.execute(cmd)
+    if out.return_code:
+        fail("failed %s: %s" % (" ".join(cmd), out.stderr))
 
-  _bash(ctx, " && " .join([
-    "TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
-    "TZ=UTC",
-    "export UTC",
-    "cd $TMP",
-    "mkdir bower_components",
-    "cd bower_components",
-    "unzip %s" % ctx.path(download_name),
-    "cd ..",
-    "find . -exec touch -t 198001010000 '{}' ';'",
-    "zip -Xr %s bower_components" % renamed_name,
-    "cd ..",
-    "rm -rf ${TMP}",
-  ]))
+    _bash(ctx, " && ".join([
+        "TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
+        "TZ=UTC",
+        "export UTC",
+        "cd $TMP",
+        "mkdir bower_components",
+        "cd bower_components",
+        "unzip %s" % ctx.path(download_name),
+        "cd ..",
+        "find . -exec touch -t 198001010000 '{}' ';'",
+        "zip -Xr %s bower_components" % renamed_name,
+        "cd ..",
+        "rm -rf ${TMP}",
+    ]))
 
-  dep_version = ctx.attr.semver if ctx.attr.semver else ctx.attr.version
-  ctx.file(version_name,
-           '"%s":"%s#%s"' % (ctx.name, ctx.attr.package, dep_version))
-  ctx.file(
-    "BUILD",
-    "\n".join([
-      "package(default_visibility=['//visibility:public'])",
-      "filegroup(name = 'zipfile', srcs = ['%s'], )" % download_name,
-      "filegroup(name = 'version_json', srcs = ['%s'], visibility=['//visibility:public'])" % version_name,
-    ]), False)
+    dep_version = ctx.attr.semver if ctx.attr.semver else ctx.attr.version
+    ctx.file(
+        version_name,
+        '"%s":"%s#%s"' % (ctx.name, ctx.attr.package, dep_version),
+    )
+    ctx.file(
+        "BUILD",
+        "\n".join([
+            "package(default_visibility=['//visibility:public'])",
+            "filegroup(name = 'zipfile', srcs = ['%s'], )" % download_name,
+            "filegroup(name = 'version_json', srcs = ['%s'], visibility=['//visibility:public'])" % version_name,
+        ]),
+        False,
+    )
 
 def _bash(ctx, cmd):
-  cmd_list = ["bash", "-c", cmd]
-  out = ctx.execute(cmd_list)
-  if out.return_code:
-    fail("failed %s: %s" % (" ".join(cmd_list), out.stderr))
+    cmd_list = ["bash", "-c", cmd]
+    out = ctx.execute(cmd_list)
+    if out.return_code:
+        fail("failed %s: %s" % (" ".join(cmd_list), out.stderr))
 
 bower_archive = repository_rule(
     _bower_archive,
@@ -119,26 +131,26 @@
 )
 
 def _bower_component_impl(ctx):
-  transitive_zipfiles = depset([ctx.file.zipfile])
-  for d in ctx.attr.deps:
-    transitive_zipfiles += d.transitive_zipfiles
+    transitive_zipfiles = depset([ctx.file.zipfile])
+    for d in ctx.attr.deps:
+        transitive_zipfiles += d.transitive_zipfiles
 
-  transitive_licenses = depset()
-  if ctx.file.license:
-    transitive_licenses += depset([ctx.file.license])
+    transitive_licenses = depset()
+    if ctx.file.license:
+        transitive_licenses += depset([ctx.file.license])
 
-  for d in ctx.attr.deps:
-    transitive_licenses += d.transitive_licenses
+    for d in ctx.attr.deps:
+        transitive_licenses += d.transitive_licenses
 
-  transitive_versions = depset(ctx.files.version_json)
-  for d in ctx.attr.deps:
-    transitive_versions += d.transitive_versions
+    transitive_versions = depset(ctx.files.version_json)
+    for d in ctx.attr.deps:
+        transitive_versions += d.transitive_versions
 
-  return struct(
-    transitive_zipfiles=transitive_zipfiles,
-    transitive_versions=transitive_versions,
-    transitive_licenses=transitive_licenses,
-  )
+    return struct(
+        transitive_licenses = transitive_licenses,
+        transitive_versions = transitive_versions,
+        transitive_zipfiles = transitive_zipfiles,
+    )
 
 _common_attrs = {
     "deps": attr.label_list(providers = [
@@ -149,35 +161,37 @@
 }
 
 def _js_component(ctx):
-  dir = ctx.outputs.zip.path + ".dir"
-  name = ctx.outputs.zip.basename
-  if name.endswith(".zip"):
-    name = name[:-4]
-  dest = "%s/%s" % (dir, name)
-  cmd = " && ".join([
-    "TZ=UTC",
-    "export TZ",
-    "mkdir -p %s" % dest,
-    "cp %s %s/" % (' '.join([s.path for s in ctx.files.srcs]), dest),
-    "cd %s" % dir,
-    "find . -exec touch -t 198001010000 '{}' ';'",
-    "zip -Xqr ../%s *" %  ctx.outputs.zip.basename
-  ])
+    dir = ctx.outputs.zip.path + ".dir"
+    name = ctx.outputs.zip.basename
+    if name.endswith(".zip"):
+        name = name[:-4]
+    dest = "%s/%s" % (dir, name)
+    cmd = " && ".join([
+        "TZ=UTC",
+        "export TZ",
+        "mkdir -p %s" % dest,
+        "cp %s %s/" % (" ".join([s.path for s in ctx.files.srcs]), dest),
+        "cd %s" % dir,
+        "find . -exec touch -t 198001010000 '{}' ';'",
+        "zip -Xqr ../%s *" % ctx.outputs.zip.basename,
+    ])
 
-  ctx.actions.run_shell(
-    inputs = ctx.files.srcs,
-    outputs = [ctx.outputs.zip],
-    command = cmd,
-    mnemonic = "GenBowerZip")
+    ctx.actions.run_shell(
+        inputs = ctx.files.srcs,
+        outputs = [ctx.outputs.zip],
+        command = cmd,
+        mnemonic = "GenBowerZip",
+    )
 
-  licenses = depset()
-  if ctx.file.license:
-    licenses += depset([ctx.file.license])
+    licenses = depset()
+    if ctx.file.license:
+        licenses += depset([ctx.file.license])
 
-  return struct(
-    transitive_zipfiles=list([ctx.outputs.zip]),
-    transitive_versions=depset(),
-    transitive_licenses=licenses)
+    return struct(
+        transitive_licenses = licenses,
+        transitive_versions = depset(),
+        transitive_zipfiles = list([ctx.outputs.zip]),
+    )
 
 js_component = rule(
     _js_component,
@@ -203,61 +217,65 @@
 )
 
 # TODO(hanwen): make license mandatory.
-def bower_component(name, license=None, **kwargs):
-  prefix = "//lib:LICENSE-"
-  if license and not license.startswith(prefix):
-    license = prefix + license
-  _bower_component(
-    name=name,
-    license=license,
-    zipfile="@%s//:zipfile"% name,
-    version_json="@%s//:version_json" % name,
-    **kwargs)
+def bower_component(name, license = None, **kwargs):
+    prefix = "//lib:LICENSE-"
+    if license and not license.startswith(prefix):
+        license = prefix + license
+    _bower_component(
+        name = name,
+        license = license,
+        zipfile = "@%s//:zipfile" % name,
+        version_json = "@%s//:version_json" % name,
+        **kwargs
+    )
 
 def _bower_component_bundle_impl(ctx):
-  """A bunch of bower components zipped up."""
-  zips = depset()
-  for d in ctx.attr.deps:
-    zips += d.transitive_zipfiles
+    """A bunch of bower components zipped up."""
+    zips = depset()
+    for d in ctx.attr.deps:
+        zips += d.transitive_zipfiles
 
-  versions = depset()
-  for d in ctx.attr.deps:
-    versions += d.transitive_versions
+    versions = depset()
+    for d in ctx.attr.deps:
+        versions += d.transitive_versions
 
-  licenses = depset()
-  for d in ctx.attr.deps:
-    licenses += d.transitive_versions
+    licenses = depset()
+    for d in ctx.attr.deps:
+        licenses += d.transitive_versions
 
-  out_zip = ctx.outputs.zip
-  out_versions = ctx.outputs.version_json
+    out_zip = ctx.outputs.zip
+    out_versions = ctx.outputs.version_json
 
-  ctx.actions.run_shell(
-    inputs=list(zips),
-    outputs=[out_zip],
-    command=" && ".join([
-      "p=$PWD",
-      "TZ=UTC",
-      "export TZ",
-      "rm -rf %s.dir" % out_zip.path,
-      "mkdir -p %s.dir/bower_components" % out_zip.path,
-      "cd %s.dir/bower_components" % out_zip.path,
-      "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
-      "cd ..",
-      "find . -exec touch -t 198001010000 '{}' ';'",
-      "zip -Xqr $p/%s bower_components/*" % out_zip.path,
-    ]),
-    mnemonic="BowerCombine")
+    ctx.actions.run_shell(
+        inputs = list(zips),
+        outputs = [out_zip],
+        command = " && ".join([
+            "p=$PWD",
+            "TZ=UTC",
+            "export TZ",
+            "rm -rf %s.dir" % out_zip.path,
+            "mkdir -p %s.dir/bower_components" % out_zip.path,
+            "cd %s.dir/bower_components" % out_zip.path,
+            "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
+            "cd ..",
+            "find . -exec touch -t 198001010000 '{}' ';'",
+            "zip -Xqr $p/%s bower_components/*" % out_zip.path,
+        ]),
+        mnemonic = "BowerCombine",
+    )
 
-  ctx.actions.run_shell(
-    inputs=list(versions),
-    outputs=[out_versions],
-    mnemonic="BowerVersions",
-    command="(echo '{' ; for j in  %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path))
+    ctx.actions.run_shell(
+        inputs = list(versions),
+        outputs = [out_versions],
+        mnemonic = "BowerVersions",
+        command = "(echo '{' ; for j in  %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path),
+    )
 
-  return struct(
-    transitive_zipfiles=zips,
-    transitive_versions=versions,
-    transitive_licenses=licenses)
+    return struct(
+        transitive_licenses = licenses,
+        transitive_versions = versions,
+        transitive_zipfiles = zips,
+    )
 
 bower_component_bundle = rule(
     _bower_component_bundle_impl,
@@ -279,80 +297,99 @@
 """
 
 def _vulcanize_impl(ctx):
-  # intermediate artifact if split is wanted.
-  if ctx.attr.split:
-    vulcanized = ctx.new_file(
-      ctx.configuration.genfiles_dir, ctx.outputs.html, ".vulcanized.html")
-  else:
-    vulcanized = ctx.outputs.html
-  destdir = ctx.outputs.html.path + ".dir"
-  zips =  [z for d in ctx.attr.deps for z in d.transitive_zipfiles ]
+    # intermediate artifact if split is wanted.
+    if ctx.attr.split:
+        vulcanized = ctx.new_file(
+            ctx.configuration.genfiles_dir,
+            ctx.outputs.html,
+            ".vulcanized.html",
+        )
+    else:
+        vulcanized = ctx.outputs.html
+    destdir = ctx.outputs.html.path + ".dir"
+    zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles]
 
-  hermetic_npm_binary = " ".join([
-    'python',
-    "$p/" + ctx.file._run_npm.path,
-    "$p/" + ctx.file._vulcanize_archive.path,
-    '--inline-scripts',
-    '--inline-css',
-    '--strip-comments',
-    '--out-html', "$p/" + vulcanized.path,
-    ctx.file.app.path
-  ])
+    hermetic_npm_binary = " ".join([
+        "python",
+        "$p/" + ctx.file._run_npm.path,
+        "$p/" + ctx.file._vulcanize_archive.path,
+        "--inline-scripts",
+        "--inline-css",
+        "--strip-comments",
+        "--out-html",
+        "$p/" + vulcanized.path,
+        ctx.file.app.path,
+    ])
 
-  pkg_dir = ctx.attr.pkg.lstrip("/")
-  cmd = " && ".join([
-    # unpack dependencies.
-    "export PATH",
-    "p=$PWD",
-    "rm -rf %s" % destdir,
-    "mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
-    "for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
-      ' '.join([z.path for z in zips]), destdir, pkg_dir),
-    "tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
-    "cd %s" % destdir,
-    hermetic_npm_binary,
-  ])
+    pkg_dir = ctx.attr.pkg.lstrip("/")
+    cmd = " && ".join([
+        # unpack dependencies.
+        "export PATH",
+        "p=$PWD",
+        "rm -rf %s" % destdir,
+        "mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
+        "for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
+            " ".join([z.path for z in zips]),
+            destdir,
+            pkg_dir,
+        ),
+        "tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
+        "cd %s" % destdir,
+        hermetic_npm_binary,
+    ])
 
-  # Node/NPM is not (yet) hermeticized, so we have to get the binary
-  # from the environment, and it may be under $HOME, so we can't run
-  # in the sandbox.
-  node_tweaks = dict(
-    use_default_shell_env = True,
-    execution_requirements = {"local": "1"},
-  )
-  ctx.actions.run_shell(
-    mnemonic = "Vulcanize",
-    inputs = [ctx.file._run_npm, ctx.file.app,
-              ctx.file._vulcanize_archive
-    ] + list(zips) + ctx.files.srcs,
-    outputs = [vulcanized],
-    command = cmd,
-    **node_tweaks)
-
-  if ctx.attr.split:
-    hermetic_npm_command = "export PATH && " + " ".join([
-      'python',
-      ctx.file._run_npm.path,
-      ctx.file._crisper_archive.path,
-      "--always-write-script",
-      "--source", vulcanized.path,
-      "--html", ctx.outputs.html.path,
-      "--js", ctx.outputs.js.path])
-
+    # Node/NPM is not (yet) hermeticized, so we have to get the binary
+    # from the environment, and it may be under $HOME, so we can't run
+    # in the sandbox.
+    node_tweaks = dict(
+        execution_requirements = {"local": "1"},
+        use_default_shell_env = True,
+    )
     ctx.actions.run_shell(
-      mnemonic = "Crisper",
-      inputs = [ctx.file._run_npm, ctx.file.app,
-                ctx.file._crisper_archive, vulcanized],
-      outputs = [ctx.outputs.js, ctx.outputs.html],
-      command = hermetic_npm_command,
-      **node_tweaks)
+        mnemonic = "Vulcanize",
+        inputs = [
+            ctx.file._run_npm,
+            ctx.file.app,
+            ctx.file._vulcanize_archive,
+        ] + list(zips) + ctx.files.srcs,
+        outputs = [vulcanized],
+        command = cmd,
+        **node_tweaks
+    )
+
+    if ctx.attr.split:
+        hermetic_npm_command = "export PATH && " + " ".join([
+            "python",
+            ctx.file._run_npm.path,
+            ctx.file._crisper_archive.path,
+            "--always-write-script",
+            "--source",
+            vulcanized.path,
+            "--html",
+            ctx.outputs.html.path,
+            "--js",
+            ctx.outputs.js.path,
+        ])
+
+        ctx.actions.run_shell(
+            mnemonic = "Crisper",
+            inputs = [
+                ctx.file._run_npm,
+                ctx.file.app,
+                ctx.file._crisper_archive,
+                vulcanized,
+            ],
+            outputs = [ctx.outputs.js, ctx.outputs.html],
+            command = hermetic_npm_command,
+            **node_tweaks
+        )
 
 def _vulcanize_output_func(name, split):
-  _ignore = [name]  # unused.
-  out = {"html": "%{name}.html"}
-  if split:
-    out["js"] = "%{name}.js"
-  return out
+    _ignore = [name]  # unused.
+    out = {"html": "%{name}.html"}
+    if split:
+        out["js"] = "%{name}.js"
+    return out
 
 _vulcanize_rule = rule(
     _vulcanize_impl,
@@ -388,9 +425,96 @@
 )
 
 def vulcanize(*args, **kwargs):
-  """Vulcanize runs vulcanize and (optionally) crisper on a set of sources."""
-  _vulcanize_rule(*args, pkg=PACKAGE_NAME, **kwargs)
+    """Vulcanize runs vulcanize and (optionally) crisper on a set of sources."""
+    _vulcanize_rule(*args, pkg = PACKAGE_NAME, **kwargs)
 
-def polygerrit_plugin(*args, **kwargs):
-  """Bundles plugin dependencies for deployment."""
-  _vulcanize_rule(*args, pkg=PACKAGE_NAME, **kwargs)
+def polygerrit_plugin(name, app, srcs = [], assets = None, **kwargs):
+    """Bundles plugin dependencies for deployment.
+
+    This rule bundles all Polymer elements and JS dependencies into .html and .js files.
+    Run-time dependencies (e.g. JS libraries loaded after plugin starts) should be provided using "assets" property.
+    Output of this rule is a FileSet with "${name}_fs", with deploy artifacts in "plugins/${name}/static".
+
+    Args:
+      name: String, plugin name.
+      app: String, the main or root source file.
+      assets: Fileset, additional files to be used by plugin in runtime, exported to "plugins/${name}/static".
+      srcs: Source files required for combining.
+    """
+
+    # Combines all .js and .html files into foo_combined.js and foo_combined.html
+    _vulcanize_rule(
+        name = name + "_combined",
+        app = app,
+        srcs = srcs if app in srcs else srcs + [app],
+        pkg = PACKAGE_NAME,
+        **kwargs
+    )
+
+    closure_js_binary(
+        name = name + "_bin",
+        compilation_level = "SIMPLE",
+        defs = [
+            "--polymer_version=1",
+            "--language_out=ECMASCRIPT6",
+            "--rewrite_polyfills=false",
+        ],
+        deps = [
+            name + "_closure_lib",
+        ],
+    )
+
+    closure_js_library(
+        name = name + "_closure_lib",
+        srcs = [name + "_combined.js"],
+        convention = "GOOGLE",
+        no_closure_library = True,
+        deps = [
+            "//lib/polymer_externs:polymer_closure",
+            "//polygerrit-ui/externs:plugin",
+        ],
+    )
+
+    native.genrule(
+        name = name + "_rename_html",
+        srcs = [name + "_combined.html"],
+        outs = [name + ".html"],
+        cmd = "sed 's/<script src=\"" + name + "_combined.js\"/<script src=\"" + name + ".js\"/g' $(SRCS) > $(OUTS)",
+        output_to_bindir = True,
+    )
+
+    native.genrule(
+        name = name + "_rename_js",
+        srcs = [name + "_bin.js"],
+        outs = [name + ".js"],
+        cmd = "cp $< $@",
+        output_to_bindir = True,
+    )
+
+    static_files = [
+        name + ".js",
+        name + ".html",
+    ]
+
+    if assets:
+        nested, direct = [], []
+        for x in assets:
+            target = nested if "/" in x else direct
+            target.append(x)
+
+        static_files += direct
+
+        if nested:
+            native.genrule(
+                name = name + "_copy_assets",
+                srcs = assets,
+                outs = [f.split("/")[-1] for f in nested],
+                cmd = "cp $(SRCS) $(@D)",
+                output_to_bindir = True,
+            )
+            static_files += [":" + name + "_copy_assets"]
+
+    native.filegroup(
+        name = name,
+        srcs = static_files,
+    )
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 1abcc23..d711356 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -43,15 +43,17 @@
         if findex != -1:
             break
     if findex == -1:
-        fail("%s does not contain any of %s",
-                         fname, _PREFIXES)
+        fail("%s does not contain any of %s" % (fname, _PREFIXES))
     return ".".join(toks[findex:]) + ".class"
 
 def _impl(ctx):
     classes = ",".join(
-        [_AsClassName(x) for x in ctx.attr.srcs])
-    ctx.file_action(output=ctx.outputs.out, content=_OUTPUT % (
-            classes, ctx.attr.outname))
+        [_AsClassName(x) for x in ctx.attr.srcs],
+    )
+    ctx.file_action(output = ctx.outputs.out, content = _OUTPUT % (
+        classes,
+        ctx.attr.outname,
+    ))
 
 _GenSuite = rule(
     attrs = {
@@ -64,9 +66,11 @@
 
 def junit_tests(name, srcs, **kwargs):
     s_name = name + "TestSuite"
-    _GenSuite(name = s_name,
-              srcs = srcs,
-              outname = s_name)
+    _GenSuite(
+        name = s_name,
+        srcs = srcs,
+        outname = s_name,
+    )
     jvm_flags = kwargs.get("jvm_flags", [])
     jvm_flags = jvm_flags + select({
         "//:java9": [
@@ -78,7 +82,9 @@
         ],
         "//conditions:default": [],
     })
-    native.java_test(name = name,
-                     test_class = s_name,
-                     srcs = srcs + [":"+s_name],
-                     **dict(kwargs, jvm_flags=jvm_flags))
+    native.java_test(
+        name = name,
+        test_class = s_name,
+        srcs = srcs + [":" + s_name],
+        **dict(kwargs, jvm_flags = jvm_flags)
+    )
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 38dfbe5..f011446 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -1,57 +1,57 @@
 def normalize_target_name(target):
-  return target.replace("//", "").replace("/", "__").replace(":", "___")
+    return target.replace("//", "").replace("/", "__").replace(":", "___")
 
 def license_map(name, targets = [], opts = [], **kwargs):
-  """Generate XML for all targets that depend directly on a LICENSE file"""
-  xmls = []
-  tools = [ "//tools/bzl:license-map.py", "//lib:all-licenses" ]
-  for target in targets:
-    subname = name + "_" + normalize_target_name(target) + ".xml"
-    xmls.append("$(location :%s)" % subname)
-    tools.append(subname)
-    native.genquery(
-      name = subname,
-      scope = [ target ],
+    """Generate XML for all targets that depend directly on a LICENSE file"""
+    xmls = []
+    tools = ["//tools/bzl:license-map.py", "//lib:all-licenses"]
+    for target in targets:
+        subname = name + "_" + normalize_target_name(target) + ".xml"
+        xmls.append("$(location :%s)" % subname)
+        tools.append(subname)
+        native.genquery(
+            name = subname,
+            scope = [target],
 
-      # Find everything that depends on a license file, but remove
-      # the license files themselves from this list.
-      expression = 'rdeps(%s, filter("//lib:LICENSE.*", deps(%s)),1) - filter("//lib:LICENSE.*", deps(%s))' % (target, target, target),
+            # Find everything that depends on a license file, but remove
+            # the license files themselves from this list.
+            expression = 'rdeps(%s, filter("//lib:LICENSE.*", deps(%s)),1) - filter("//lib:LICENSE.*", deps(%s))' % (target, target, target),
 
-      # We are interested in the edges of the graph ({java_library,
-      # license-file} tuples).  'query' provides this in the XML output.
-      opts = [ "--output=xml", ],
+            # We are interested in the edges of the graph ({java_library,
+            # license-file} tuples).  'query' provides this in the XML output.
+            opts = ["--output=xml"],
+        )
+
+    # post process the XML into our favorite format.
+    native.genrule(
+        name = "gen_license_txt_" + name,
+        cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+        outs = [name + ".txt"],
+        tools = tools,
+        **kwargs
     )
 
-  # post process the XML into our favorite format.
-  native.genrule(
-    name = "gen_license_txt_" + name,
-    cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
-    outs = [ name + ".txt" ],
-    tools = tools,
-    **kwargs
-  )
-
 def license_test(name, target):
-  """Make sure a target doesn't depend on DO_NOT_DISTRIBUTE license"""
-  txt = name + "-forbidden.txt"
+    """Make sure a target doesn't depend on DO_NOT_DISTRIBUTE license"""
+    txt = name + "-forbidden.txt"
 
-  # fully qualify target name.
-  if target[0] not in ":/":
-    target = ":" + target
-  if target[0] != "/":
-    target = "//" + PACKAGE_NAME + target
+    # fully qualify target name.
+    if target[0] not in ":/":
+        target = ":" + target
+    if target[0] != "/":
+        target = "//" + PACKAGE_NAME + target
 
-  forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
-  native.genquery(
-    name = txt,
-    scope = [ target, forbidden ],
-    # Find everything that depends on a license file, but remove
-    # the license files themselves from this list.
-    expression = 'rdeps(%s, "%s", 1) - rdeps(%s, "%s", 0)' % (target, forbidden, target, forbidden),
-  )
-  native.sh_test(
-    name = name,
-    srcs = [ "//tools/bzl:test_license.sh" ],
-    args  = [ "$(location :%s)" % txt ],
-    data = [ txt ],
-  )
+    forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
+    native.genquery(
+        name = txt,
+        scope = [target, forbidden],
+        # Find everything that depends on a license file, but remove
+        # the license files themselves from this list.
+        expression = 'rdeps(%s, "%s", 1) - rdeps(%s, "%s", 0)' % (target, forbidden, target, forbidden),
+    )
+    native.sh_test(
+        name = name,
+        srcs = ["//tools/bzl:test_license.sh"],
+        args = ["$(location :%s)" % txt],
+        data = [txt],
+    )
diff --git a/tools/bzl/maven.bzl b/tools/bzl/maven.bzl
index c255c0c..71aa91c 100644
--- a/tools/bzl/maven.bzl
+++ b/tools/bzl/maven.bzl
@@ -15,18 +15,18 @@
 # Merge maven files
 
 def cmd(jars):
-  return ('$(location //tools:merge_jars) $@ '
-          + ' '.join(['$(location %s)' % j for j in jars]))
+    return ("$(location //tools:merge_jars) $@ " +
+            " ".join(["$(location %s)" % j for j in jars]))
 
 def merge_maven_jars(name, srcs, **kwargs):
-  native.genrule(
-    name = '%s__merged_bin' % name,
-    cmd = cmd(srcs),
-    tools = srcs + ['//tools:merge_jars'],
-    outs = ['%s__merged.jar' % name],
-  )
-  native.java_import(
-    name = name,
-    jars = [':%s__merged_bin' % name],
-    **kwargs
-  )
+    native.genrule(
+        name = "%s__merged_bin" % name,
+        cmd = cmd(srcs),
+        tools = srcs + ["//tools:merge_jars"],
+        outs = ["%s__merged.jar" % name],
+    )
+    native.java_import(
+        name = name,
+        jars = [":%s__merged_bin" % name],
+        **kwargs
+    )
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 55bfae1..2ebb2c2 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -7,69 +7,70 @@
 MAVEN_LOCAL = "MAVEN_LOCAL:"
 
 def _maven_release(ctx, parts):
-  """induce jar and url name from maven coordinates."""
-  if len(parts) not in [3, 4]:
-    fail('%s:\nexpected id="groupId:artifactId:version[:classifier]"'
-         % ctx.attr.artifact)
-  if len(parts) == 4:
-    group, artifact, version, classifier = parts
-    file_version = version + '-' + classifier
-  else:
-    group, artifact, version = parts
-    file_version = version
+    """induce jar and url name from maven coordinates."""
+    if len(parts) not in [3, 4]:
+        fail('%s:\nexpected id="groupId:artifactId:version[:classifier]"' %
+             ctx.attr.artifact)
+    if len(parts) == 4:
+        group, artifact, version, classifier = parts
+        file_version = version + "-" + classifier
+    else:
+        group, artifact, version = parts
+        file_version = version
 
-  jar = artifact.lower() + '-' + file_version
-  url = '/'.join([
-    ctx.attr.repository,
-    group.replace('.', '/'),
-    artifact,
-    version,
-    artifact + '-' + file_version])
+    jar = artifact.lower() + "-" + file_version
+    url = "/".join([
+        ctx.attr.repository,
+        group.replace(".", "/"),
+        artifact,
+        version,
+        artifact + "-" + file_version,
+    ])
 
-  return jar, url
+    return jar, url
 
 # Creates a struct containing the different parts of an artifact's FQN
 def _create_coordinates(fully_qualified_name):
-  parts = fully_qualified_name.split(":")
-  packaging = None
-  classifier = None
+    parts = fully_qualified_name.split(":")
+    packaging = None
+    classifier = None
 
-  if len(parts) == 3:
-    group_id, artifact_id, version = parts
-  elif len(parts) == 4:
-    group_id, artifact_id, version, packaging = parts
-  elif len(parts) == 5:
-    group_id, artifact_id, version, packaging, classifier = parts
-  else:
-    fail("Invalid fully qualified name for artifact: %s" % fully_qualified_name)
+    if len(parts) == 3:
+        group_id, artifact_id, version = parts
+    elif len(parts) == 4:
+        group_id, artifact_id, version, packaging = parts
+    elif len(parts) == 5:
+        group_id, artifact_id, version, packaging, classifier = parts
+    else:
+        fail("Invalid fully qualified name for artifact: %s" % fully_qualified_name)
 
-  return struct(
-      fully_qualified_name = fully_qualified_name,
-      group_id = group_id,
-      artifact_id = artifact_id,
-      packaging = packaging,
-      classifier = classifier,
-      version = version,
-  )
+    return struct(
+        fully_qualified_name = fully_qualified_name,
+        group_id = group_id,
+        artifact_id = artifact_id,
+        packaging = packaging,
+        classifier = classifier,
+        version = version,
+    )
 
 def _format_deps(attr, deps):
-  formatted_deps = ""
-  if deps:
-    if len(deps) == 1:
-      formatted_deps += "%s = [\'%s\']," % (attr, deps[0])
-    else:
-      formatted_deps += "%s = [\n" % attr
-      for dep in deps:
-        formatted_deps += "        \'%s\',\n" % dep
-      formatted_deps += "    ],"
-  return formatted_deps
+    formatted_deps = ""
+    if deps:
+        if len(deps) == 1:
+            formatted_deps += "%s = [\'%s\']," % (attr, deps[0])
+        else:
+            formatted_deps += "%s = [\n" % attr
+            for dep in deps:
+                formatted_deps += "        \'%s\',\n" % dep
+            formatted_deps += "    ],"
+    return formatted_deps
 
 def _generate_build_files(ctx, binjar, srcjar):
-  header = "# DO NOT EDIT: automatically generated BUILD file for maven_jar rule %s" % ctx.name
-  srcjar_attr = ""
-  if srcjar:
-    srcjar_attr = 'srcjar = "%s",' % srcjar
-  contents = """
+    header = "# DO NOT EDIT: automatically generated BUILD file for maven_jar rule %s" % ctx.name
+    srcjar_attr = ""
+    if srcjar:
+        srcjar_attr = 'srcjar = "%s",' % srcjar
+    contents = """
 {header}
 package(default_visibility = ['//visibility:public'])
 java_import(
@@ -86,22 +87,24 @@
     {deps}
     {exports}
 )
-\n""".format(srcjar_attr = srcjar_attr,
-             header = header,
-             binjar = binjar,
-             deps = _format_deps("deps", ctx.attr.deps),
-             exports = _format_deps("exports", ctx.attr.exports))
-  if srcjar:
-    contents += """
+\n""".format(
+        srcjar_attr = srcjar_attr,
+        header = header,
+        binjar = binjar,
+        deps = _format_deps("deps", ctx.attr.deps),
+        exports = _format_deps("exports", ctx.attr.exports),
+    )
+    if srcjar:
+        contents += """
 java_import(
     name = 'src',
     jars = ['{srcjar}'],
 )
 """.format(srcjar = srcjar)
-  ctx.file('%s/BUILD' % ctx.path("jar"), contents, False)
+    ctx.file("%s/BUILD" % ctx.path("jar"), contents, False)
 
-  # Compatibility layer for java_import_external from rules_closure
-  contents = """
+    # Compatibility layer for java_import_external from rules_closure
+    contents = """
 {header}
 package(default_visibility = ['//visibility:public'])
 
@@ -110,52 +113,53 @@
     actual = "@{rule_name}//jar",
 )
 \n""".format(rule_name = ctx.name, header = header)
-  ctx.file("BUILD", contents, False)
+    ctx.file("BUILD", contents, False)
 
 def _maven_jar_impl(ctx):
-  """rule to download a Maven archive."""
-  coordinates = _create_coordinates(ctx.attr.artifact)
+    """rule to download a Maven archive."""
+    coordinates = _create_coordinates(ctx.attr.artifact)
 
-  name = ctx.name
-  sha1 = ctx.attr.sha1
+    name = ctx.name
+    sha1 = ctx.attr.sha1
 
-  parts = ctx.attr.artifact.split(':')
-  # TODO(davido): Only releases for now, implement handling snapshots
-  jar, url = _maven_release(ctx, parts)
+    parts = ctx.attr.artifact.split(":")
 
-  binjar = jar + '.jar'
-  binjar_path = ctx.path('/'.join(['jar', binjar]))
-  binurl = url + '.jar'
+    # TODO(davido): Only releases for now, implement handling snapshots
+    jar, url = _maven_release(ctx, parts)
 
-  python = ctx.which("python")
-  script = ctx.path(ctx.attr._download_script)
+    binjar = jar + ".jar"
+    binjar_path = ctx.path("/".join(["jar", binjar]))
+    binurl = url + ".jar"
 
-  args = [python, script, "-o", binjar_path, "-u", binurl]
-  if ctx.attr.sha1:
-    args.extend(["-v", sha1])
-  if ctx.attr.unsign:
-    args.append('--unsign')
-  for x in ctx.attr.exclude:
-    args.extend(['-x', x])
+    python = ctx.which("python")
+    script = ctx.path(ctx.attr._download_script)
 
-  out = ctx.execute(args)
+    args = [python, script, "-o", binjar_path, "-u", binurl]
+    if ctx.attr.sha1:
+        args.extend(["-v", sha1])
+    if ctx.attr.unsign:
+        args.append("--unsign")
+    for x in ctx.attr.exclude:
+        args.extend(["-x", x])
 
-  if out.return_code:
-    fail("failed %s: %s" % (' '.join(args), out.stderr))
-
-  srcjar = None
-  if ctx.attr.src_sha1 or ctx.attr.attach_source:
-    srcjar = jar + '-src.jar'
-    srcurl = url + '-sources.jar'
-    srcjar_path = ctx.path('jar/' + srcjar)
-    args = [python, script, "-o", srcjar_path, "-u", srcurl]
-    if ctx.attr.src_sha1:
-      args.extend(['-v', ctx.attr.src_sha1])
     out = ctx.execute(args)
-    if out.return_code:
-      fail("failed %s: %s" % (args, out.stderr))
 
-  _generate_build_files(ctx, binjar, srcjar)
+    if out.return_code:
+        fail("failed %s: %s" % (" ".join(args), out.stderr))
+
+    srcjar = None
+    if ctx.attr.src_sha1 or ctx.attr.attach_source:
+        srcjar = jar + "-src.jar"
+        srcurl = url + "-sources.jar"
+        srcjar_path = ctx.path("jar/" + srcjar)
+        args = [python, script, "-o", srcjar_path, "-u", srcurl]
+        if ctx.attr.src_sha1:
+            args.extend(["-v", ctx.attr.src_sha1])
+        out = ctx.execute(args)
+        if out.return_code:
+            fail("failed %s: %s" % (args, out.stderr))
+
+    _generate_build_files(ctx, binjar, srcjar)
 
 maven_jar = repository_rule(
     attrs = {
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 46a4f9b..1a376e9 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -32,93 +32,93 @@
 ]
 
 def _add_context(in_file, output):
-  input_path = in_file.path
-  return [
-    'unzip -qd %s %s' % (output, input_path)
-  ]
+    input_path = in_file.path
+    return [
+        "unzip -qd %s %s" % (output, input_path),
+    ]
 
 def _add_file(in_file, output):
-  output_path = output
-  input_path = in_file.path
-  short_path = in_file.short_path
-  n = in_file.basename
+    output_path = output
+    input_path = in_file.path
+    short_path = in_file.short_path
+    n = in_file.basename
 
-  if short_path.startswith('gerrit-'):
-    n = short_path.split('/')[0] + '-' + n
-  elif short_path.startswith('java/'):
-    n = short_path[5:].replace('/', '_')
-  output_path += n
-  return [
-    'test -L %s || ln -s $(pwd)/%s %s' % (output_path, input_path, output_path)
-  ]
+    if short_path.startswith("gerrit-"):
+        n = short_path.split("/")[0] + "-" + n
+    elif short_path.startswith("java/"):
+        n = short_path[5:].replace("/", "_")
+    output_path += n
+    return [
+        "test -L %s || ln -s $(pwd)/%s %s" % (output_path, input_path, output_path),
+    ]
 
 def _make_war(input_dir, output):
-  return '(%s)' % ' && '.join([
-    'root=$(pwd)',
-    'TZ=UTC',
-    'export TZ',
-    'cd %s' % input_dir,
-    "find . -exec touch -t 198001010000 '{}' ';' 2> /dev/null",
-    'zip -X -9qr ${root}/%s .' % (output.path),
-  ])
+    return "(%s)" % " && ".join([
+        "root=$(pwd)",
+        "TZ=UTC",
+        "export TZ",
+        "cd %s" % input_dir,
+        "find . -exec touch -t 198001010000 '{}' ';' 2> /dev/null",
+        "zip -X -9qr ${root}/%s ." % (output.path),
+    ])
 
 def _war_impl(ctx):
-  war = ctx.outputs.war
-  build_output = war.path + '.build_output'
-  inputs = []
+    war = ctx.outputs.war
+    build_output = war.path + ".build_output"
+    inputs = []
 
-  # Create war layout
-  cmd = [
-    'set -e;rm -rf ' + build_output,
-    'mkdir -p ' + build_output,
-    'mkdir -p %s/WEB-INF/lib' % build_output,
-    'mkdir -p %s/WEB-INF/pgm-lib' % build_output,
-  ]
+    # Create war layout
+    cmd = [
+        "set -e;rm -rf " + build_output,
+        "mkdir -p " + build_output,
+        "mkdir -p %s/WEB-INF/lib" % build_output,
+        "mkdir -p %s/WEB-INF/pgm-lib" % build_output,
+    ]
 
-  # Add lib
-  transitive_lib_deps = depset()
-  for l in ctx.attr.libs:
-    if hasattr(l, 'java'):
-      transitive_lib_deps += l.java.transitive_runtime_deps
-    elif hasattr(l, 'files'):
-      transitive_lib_deps += l.files
+    # Add lib
+    transitive_lib_deps = depset()
+    for l in ctx.attr.libs:
+        if hasattr(l, "java"):
+            transitive_lib_deps += l.java.transitive_runtime_deps
+        elif hasattr(l, "files"):
+            transitive_lib_deps += l.files
 
-  for dep in transitive_lib_deps:
-    cmd += _add_file(dep, build_output + '/WEB-INF/lib/')
-    inputs.append(dep)
+    for dep in transitive_lib_deps:
+        cmd += _add_file(dep, build_output + "/WEB-INF/lib/")
+        inputs.append(dep)
 
-  # Add pgm lib
-  transitive_pgmlib_deps = depset()
-  for l in ctx.attr.pgmlibs:
-    transitive_pgmlib_deps += l.java.transitive_runtime_deps
+    # Add pgm lib
+    transitive_pgmlib_deps = depset()
+    for l in ctx.attr.pgmlibs:
+        transitive_pgmlib_deps += l.java.transitive_runtime_deps
 
-  for dep in transitive_pgmlib_deps:
-    if dep not in inputs:
-      cmd += _add_file(dep, build_output + '/WEB-INF/pgm-lib/')
-      inputs.append(dep)
+    for dep in transitive_pgmlib_deps:
+        if dep not in inputs:
+            cmd += _add_file(dep, build_output + "/WEB-INF/pgm-lib/")
+            inputs.append(dep)
 
-  # Add context
-  transitive_context_deps = depset()
-  if ctx.attr.context:
-    for jar in ctx.attr.context:
-      if hasattr(jar, 'java'):
-        transitive_context_deps += jar.java.transitive_runtime_deps
-      elif hasattr(jar, 'files'):
-        transitive_context_deps += jar.files
-  for dep in transitive_context_deps:
-    cmd += _add_context(dep, build_output)
-    inputs.append(dep)
+    # Add context
+    transitive_context_deps = depset()
+    if ctx.attr.context:
+        for jar in ctx.attr.context:
+            if hasattr(jar, "java"):
+                transitive_context_deps += jar.java.transitive_runtime_deps
+            elif hasattr(jar, "files"):
+                transitive_context_deps += jar.files
+    for dep in transitive_context_deps:
+        cmd += _add_context(dep, build_output)
+        inputs.append(dep)
 
-  # Add zip war
-  cmd.append(_make_war(build_output, war))
+    # Add zip war
+    cmd.append(_make_war(build_output, war))
 
-  ctx.actions.run_shell(
-    inputs = inputs,
-    outputs = [war],
-    mnemonic = 'WAR',
-    command = '\n'.join(cmd),
-    use_default_shell_env = True,
-  )
+    ctx.actions.run_shell(
+        inputs = inputs,
+        outputs = [war],
+        mnemonic = "WAR",
+        command = "\n".join(cmd),
+        use_default_shell_env = True,
+    )
 
 # context: go to the root directory
 # libs: go to the WEB-INF/lib directory
@@ -133,25 +133,25 @@
     implementation = _war_impl,
 )
 
-def pkg_war(name, ui = 'ui_optdbg', context = [], doc = False, **kwargs):
-  doc_ctx = []
-  doc_lib = []
-  ui_deps = []
-  if ui == 'polygerrit' or ui == 'ui_optdbg' or ui == 'ui_optdbg_r':
-    ui_deps.append('//polygerrit-ui/app:polygerrit_ui')
-  if ui and ui != 'polygerrit':
-    ui_deps.append('//gerrit-gwtui:%s' % ui)
-  if doc:
-    doc_ctx.append('//Documentation:html')
-    doc_lib.append('//Documentation:index')
+def pkg_war(name, ui = "ui_optdbg", context = [], doc = False, **kwargs):
+    doc_ctx = []
+    doc_lib = []
+    ui_deps = []
+    if ui == "polygerrit" or ui == "ui_optdbg" or ui == "ui_optdbg_r":
+        ui_deps.append("//polygerrit-ui/app:polygerrit_ui")
+    if ui and ui != "polygerrit":
+        ui_deps.append("//gerrit-gwtui:%s" % ui)
+    if doc:
+        doc_ctx.append("//Documentation:html")
+        doc_lib.append("//Documentation:index")
 
-  _pkg_war(
-    name = name,
-    libs = LIBS + doc_lib,
-    pgmlibs = PGMLIBS,
-    context = doc_ctx + context + ui_deps + [
-      '//java:gerrit-main-class_deploy.jar',
-      '//webapp:assets',
-    ],
-    **kwargs
-  )
+    _pkg_war(
+        name = name,
+        libs = LIBS + doc_lib,
+        pgmlibs = PGMLIBS,
+        context = doc_ctx + context + ui_deps + [
+            "//java:gerrit-main-class_deploy.jar",
+            "//webapp:assets",
+        ],
+        **kwargs
+    )
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 23f88df..5ae7dd9 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,11 +1,11 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load(
     "//tools/bzl:gwt.bzl",
+    "GWT_COMPILER_ARGS",
+    "GWT_JVM_ARGS",
     "GWT_PLUGIN_DEPS",
     "GWT_PLUGIN_DEPS_NEVERLINK",
     "GWT_TRANSITIVE_DEPS",
-    "GWT_COMPILER_ARGS",
-    "GWT_JVM_ARGS",
     "gwt_binary",
 )
 
@@ -21,82 +21,84 @@
 ]
 
 def gerrit_plugin(
-    name,
-    deps = [],
-    provided_deps = [],
-    srcs = [],
-    gwt_module = [],
-    resources = [],
-    manifest_entries = [],
-    dir_name = None,
-    target_suffix = "",
-    **kwargs):
-  native.java_library(
-    name = name + '__plugin',
-    srcs = srcs,
-    resources = resources,
-    deps = provided_deps + deps + GWT_PLUGIN_DEPS_NEVERLINK + PLUGIN_DEPS_NEVERLINK,
-    visibility = ['//visibility:public'],
-    **kwargs
-  )
-
-  static_jars = []
-  if gwt_module:
-    static_jars = [':%s-static' % name]
-
-  if not dir_name:
-    dir_name = name
-
-  native.java_binary(
-    name = '%s__non_stamped' % name,
-    deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
-    main_class = 'Dummy',
-    runtime_deps = [
-      ':%s__plugin' % name,
-    ] + static_jars,
-    visibility = ['//visibility:public'],
-    **kwargs
-  )
-
-  if gwt_module:
+        name,
+        deps = [],
+        provided_deps = [],
+        srcs = [],
+        gwt_module = [],
+        resources = [],
+        manifest_entries = [],
+        dir_name = None,
+        target_suffix = "",
+        **kwargs):
     native.java_library(
-      name = name + '__gwt_module',
-      resources = depset(srcs + resources).to_list(),
-      runtime_deps = deps + GWT_PLUGIN_DEPS,
-      visibility = ['//visibility:public'],
-      **kwargs
-    )
-    genrule2(
-      name = '%s-static' % name,
-      cmd = ' && '.join([
-        'mkdir -p $$TMP/static',
-        'unzip -qd $$TMP/static $(location %s__gwt_application)' % name,
-        'cd $$TMP',
-        'zip -qr $$ROOT/$@ .']),
-      tools = [':%s__gwt_application' % name],
-      outs = ['%s-static.jar' % name],
-    )
-    gwt_binary(
-      name = name + '__gwt_application',
-      module = [gwt_module],
-      deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ['//lib/gwt:dev'],
-      module_deps = [':%s__gwt_module' % name],
-      compiler_args = GWT_COMPILER_ARGS,
-      jvm_args = GWT_JVM_ARGS,
+        name = name + "__plugin",
+        srcs = srcs,
+        resources = resources,
+        deps = provided_deps + deps + GWT_PLUGIN_DEPS_NEVERLINK + PLUGIN_DEPS_NEVERLINK,
+        visibility = ["//visibility:public"],
+        **kwargs
     )
 
-  # TODO(davido): Remove manual merge of manifest file when this feature
-  # request is implemented: https://github.com/bazelbuild/bazel/issues/2009
-  genrule2(
-    name = name + target_suffix,
-    stamp = 1,
-    srcs = ['%s__non_stamped_deploy.jar' % name],
-    cmd = " && ".join([
-      "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
-      "cd $$TMP",
-      "unzip -q $$ROOT/$<",
-      "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
-      "zip -qr $$ROOT/$@ ."]),
-    outs = ['%s%s.jar' % (name, target_suffix)],
-    visibility = ['//visibility:public'],
-  )
+    static_jars = []
+    if gwt_module:
+        static_jars = [":%s-static" % name]
+
+    if not dir_name:
+        dir_name = name
+
+    native.java_binary(
+        name = "%s__non_stamped" % name,
+        deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
+        main_class = "Dummy",
+        runtime_deps = [
+            ":%s__plugin" % name,
+        ] + static_jars,
+        visibility = ["//visibility:public"],
+        **kwargs
+    )
+
+    if gwt_module:
+        native.java_library(
+            name = name + "__gwt_module",
+            resources = depset(srcs + resources).to_list(),
+            runtime_deps = deps + GWT_PLUGIN_DEPS,
+            visibility = ["//visibility:public"],
+            **kwargs
+        )
+        genrule2(
+            name = "%s-static" % name,
+            cmd = " && ".join([
+                "mkdir -p $$TMP/static",
+                "unzip -qd $$TMP/static $(location %s__gwt_application)" % name,
+                "cd $$TMP",
+                "zip -qr $$ROOT/$@ .",
+            ]),
+            tools = [":%s__gwt_application" % name],
+            outs = ["%s-static.jar" % name],
+        )
+        gwt_binary(
+            name = name + "__gwt_application",
+            module = [gwt_module],
+            deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ["//lib/gwt:dev"],
+            module_deps = [":%s__gwt_module" % name],
+            compiler_args = GWT_COMPILER_ARGS,
+            jvm_args = GWT_JVM_ARGS,
+        )
+
+    # TODO(davido): Remove manual merge of manifest file when this feature
+    # request is implemented: https://github.com/bazelbuild/bazel/issues/2009
+    genrule2(
+        name = name + target_suffix,
+        stamp = 1,
+        srcs = ["%s__non_stamped_deploy.jar" % name],
+        cmd = " && ".join([
+            "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
+            "cd $$TMP",
+            "unzip -q $$ROOT/$<",
+            "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
+            "zip -qr $$ROOT/$@ .",
+        ]),
+        outs = ["%s%s.jar" % (name, target_suffix)],
+        visibility = ["//visibility:public"],
+    )
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index d817701..f14262a 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -15,8 +15,8 @@
 
 """This downloads an NPM binary, and bundles it with its dependencies.
 
-This is used to assemble a pinned version of crisper, hosted on the
-Google storage bucket ("repository=GERRIT" in WORKSPACE).
+For full instructions on adding new binaries to the build, see
+http://gerrit-review.googlesource.com/Documentation/dev-bazel.html#npm-binary
 """
 
 from __future__ import print_function
diff --git a/tools/maven/package.bzl b/tools/maven/package.bzl
index 3c32bb2..5b497f8 100644
--- a/tools/maven/package.bzl
+++ b/tools/maven/package.bzl
@@ -25,73 +25,86 @@
 ]))
 
 def maven_package(
-    version,
-    repository = None,
-    url = None,
-    jar = {},
-    src = {},
-    doc = {},
-    war = {}):
+        version,
+        repository = None,
+        url = None,
+        jar = {},
+        src = {},
+        doc = {},
+        war = {}):
+    build_cmd = ["bazel", "build"]
+    mvn_cmd = ["python", "tools/maven/mvn.py", "-v", version]
+    api_cmd = mvn_cmd[:]
+    api_targets = []
+    for type, d in [("jar", jar), ("java-source", src), ("javadoc", doc)]:
+        for a, t in sorted(d.items()):
+            api_cmd.append("-s %s:%s:$(location %s)" % (a, type, t))
+            api_targets.append(t)
 
-  build_cmd = ['bazel', 'build']
-  mvn_cmd = ['python', 'tools/maven/mvn.py', '-v', version]
-  api_cmd = mvn_cmd[:]
-  api_targets = []
-  for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
-    for a,t in sorted(d.items()):
-      api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
-      api_targets.append(t)
-
-  native.genrule(
-    name = 'gen_api_install',
-    cmd = sh_bang_template % (
-      ' '.join(build_cmd + api_targets),
-      ' '.join(api_cmd + ['-a', 'install'])),
-    srcs = api_targets,
-    outs = ['api_install.sh'],
-    executable = True,
-    testonly = 1,
-  )
-
-  if repository and url:
     native.genrule(
-      name = 'gen_api_deploy',
-      cmd = sh_bang_template % (
-        ' '.join(build_cmd + api_targets),
-        ' '.join(api_cmd + ['-a', 'deploy',
-                            '--repository', repository,
-                            '--url', url])),
-      srcs = api_targets,
-      outs = ['api_deploy.sh'],
-      executable = True,
-      testonly = 1,
+        name = "gen_api_install",
+        cmd = sh_bang_template % (
+            " ".join(build_cmd + api_targets),
+            " ".join(api_cmd + ["-a", "install"]),
+        ),
+        srcs = api_targets,
+        outs = ["api_install.sh"],
+        executable = True,
+        testonly = 1,
     )
 
-  war_cmd = mvn_cmd[:]
-  war_targets = []
-  for a,t in sorted(war.items()):
-    war_cmd.append('-s %s:war:$(location %s)' % (a,t))
-    war_targets.append(t)
+    if repository and url:
+        native.genrule(
+            name = "gen_api_deploy",
+            cmd = sh_bang_template % (
+                " ".join(build_cmd + api_targets),
+                " ".join(api_cmd + [
+                    "-a",
+                    "deploy",
+                    "--repository",
+                    repository,
+                    "--url",
+                    url,
+                ]),
+            ),
+            srcs = api_targets,
+            outs = ["api_deploy.sh"],
+            executable = True,
+            testonly = 1,
+        )
 
-  native.genrule(
-    name = 'gen_war_install',
-    cmd = sh_bang_template % (' '.join(build_cmd + war_targets),
-                              ' '.join(war_cmd + ['-a', 'install'])),
-    srcs = war_targets,
-    outs = ['war_install.sh'],
-    executable = True,
-  )
+    war_cmd = mvn_cmd[:]
+    war_targets = []
+    for a, t in sorted(war.items()):
+        war_cmd.append("-s %s:war:$(location %s)" % (a, t))
+        war_targets.append(t)
 
-  if repository and url:
     native.genrule(
-      name = 'gen_war_deploy',
-      cmd = sh_bang_template % (
-          ' '.join(build_cmd + war_targets),
-          ' '.join(war_cmd + [
-        '-a', 'deploy',
-        '--repository', repository,
-        '--url', url])),
-      srcs = war_targets,
-      outs = ['war_deploy.sh'],
-      executable = True,
+        name = "gen_war_install",
+        cmd = sh_bang_template % (
+            " ".join(build_cmd + war_targets),
+            " ".join(war_cmd + ["-a", "install"]),
+        ),
+        srcs = war_targets,
+        outs = ["war_install.sh"],
+        executable = True,
     )
+
+    if repository and url:
+        native.genrule(
+            name = "gen_war_deploy",
+            cmd = sh_bang_template % (
+                " ".join(build_cmd + war_targets),
+                " ".join(war_cmd + [
+                    "-a",
+                    "deploy",
+                    "--repository",
+                    repository,
+                    "--url",
+                    url,
+                ]),
+            ),
+            srcs = war_targets,
+            outs = ["war_deploy.sh"],
+            executable = True,
+        )