Merge changes I0da14436,I1e15767f

* changes:
  Remove workaround regarding newlines on GWT UI
  Fix diff regarding newline at end of file
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/dev-plugins.txt b/Documentation/dev-plugins.txt
index 505d4b7..5c008c7 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -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/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/WORKSPACE b/WORKSPACE
index 70200d1..dfe2a45 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
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index c0b4df1..960b558 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -68,6 +68,8 @@
 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;
@@ -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();
   }
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index efca382..0214cea 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -99,6 +99,7 @@
         "//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",
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/common/data/GroupReference.java b/java/com/google/gerrit/common/data/GroupReference.java
index 8060659..cfaad17 100644
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ b/java/com/google/gerrit/common/data/GroupReference.java
@@ -61,6 +61,9 @@
   }
 
   public void setName(String newName) {
+    if (newName == null) {
+      throw new NullPointerException();
+    }
     this.name = newName;
   }
 
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/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/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/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/BUILD b/java/com/google/gerrit/server/BUILD
index a67cccb..e99c686 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -41,6 +41,7 @@
         "//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/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/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/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 3375ce7..5d12e79 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -43,6 +43,7 @@
 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;
@@ -57,7 +58,6 @@
 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.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
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/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/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 44aa83b..e3638b6 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -499,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/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/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/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/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/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/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/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/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 55565ad..cf65908 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -26,6 +26,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/restapi",
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/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/common/data/GroupReferenceTest.java b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
index 4181423..717e122 100644
--- a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -18,9 +18,13 @@
 
 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";
@@ -54,6 +58,31 @@
   }
 
   @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();
@@ -82,6 +111,8 @@
     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();
   }
@@ -97,8 +128,8 @@
     groupReference.setName(name2);
     assertThat(groupReference.getName()).isEqualTo(name2);
 
+    exception.expect(NullPointerException.class);
     groupReference.setName(null);
-    assertThat(groupReference.getName()).isNull();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 7b49e1d..98e466e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -67,7 +67,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
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/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/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/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index be90acb..384469c 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -2,7 +2,7 @@
 
 _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"
 
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/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/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/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/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/js.bzl b/tools/bzl/js.bzl
index fbacfad..6590cd6 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -3,6 +3,7 @@
 GERRIT = "GERRIT:"
 
 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])
@@ -146,9 +147,9 @@
         transitive_versions += d.transitive_versions
 
     return struct(
-        transitive_zipfiles = transitive_zipfiles,
-        transitive_versions = transitive_versions,
         transitive_licenses = transitive_licenses,
+        transitive_versions = transitive_versions,
+        transitive_zipfiles = transitive_zipfiles,
     )
 
 _common_attrs = {
@@ -187,9 +188,9 @@
         licenses += depset([ctx.file.license])
 
     return struct(
-        transitive_zipfiles = list([ctx.outputs.zip]),
-        transitive_versions = depset(),
         transitive_licenses = licenses,
+        transitive_versions = depset(),
+        transitive_zipfiles = list([ctx.outputs.zip]),
     )
 
 js_component = rule(
@@ -271,9 +272,9 @@
     )
 
     return struct(
-        transitive_zipfiles = zips,
-        transitive_versions = versions,
         transitive_licenses = licenses,
+        transitive_versions = versions,
+        transitive_zipfiles = zips,
     )
 
 bower_component_bundle = rule(
@@ -341,8 +342,8 @@
     # 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"},
+        use_default_shell_env = True,
     )
     ctx.actions.run_shell(
         mnemonic = "Vulcanize",
@@ -427,6 +428,93 @@
     """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,
+    )