Merge "Fix _nextStepHandle not being assigned a value when scrolling"
diff --git a/.bazelrc b/.bazelrc
index f56b683..d6d4ce6 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -2,7 +2,10 @@
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --experimental_strict_action_env
 build --action_env=PATH
-test --build_tests_only
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
+build --java_toolchain //tools:error_prone_warnings_toolchain
+
+test --build_tests_only
+test --test_output=errors
 
 import tools/remote-bazelrc
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index ef6a488..7d46e26 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -66,6 +66,12 @@
 will be appended to emails related to a user submitting comments on changes.
 See `ChangeSubject.soy`, Comment and ChangeFooter.
 
+=== DeleteKey.soy and DeleteKeyHtml.soy
+
+DeleteKey templates will determine the contents of the email related to SSH or GPG keys
+being deleted from a user account. This notification is not sent when the key is
+administratively deleted from another user account.
+
 === DeleteVote.soy and DeleteVoteHtml.soy
 
 The DeleteVote templates will determine the contents of the email related to
@@ -83,6 +89,11 @@
 The Footer templates will determine the contents of the footer text appended to
 the end of all outgoing emails after the ChangeFooter and CommentFooter.
 
+=== HttpPasswordUpdate.soy and HttpPasswordUpdateHtml.soy
+
+HttpPasswordUpdate templates will determine the contents of the email related to adding,
+changing or deleting the HTTP password on a user account.
+
 === Merged.soy and MergedHtml.soy
 
 The Merged templates will determine the contents of the email related to a
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index c37e9d6..306fff9 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -219,13 +219,6 @@
 Note that when building an individual plugin, the `core.zip` package
 is not regenerated.
 
-To build with all Error Prone warnings activated, run:
-
-----
-  bazel build --java_toolchain //tools:error_prone_warnings_toolchain //...
-----
-
-
 [[IDEs]]
 == Using an IDE.
 
@@ -569,7 +562,7 @@
 
 
 [[RBE]]
-==== Google Remote Build Support
+== Google Remote Build Support
 
 The Bazel build can be used with Google's Remote Build Execution.
 
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 072c22c..47ace5b 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -75,6 +75,12 @@
 Some plugins describe their build process in `src/main/resources/Documentation/build.md`
 file. It may worth checking.
 
+=== Error Prone checks
+
+Error Prone checks are enabled by default for core Gerrit and all core plugins. To
+enable the checks for custom plugins, add it in the `error_prone_packages` group
+in `tools/BUILD`.
+
 === Plugins with external dependencies ===
 
 If the plugin has external dependencies, then they must be included from Gerrit's
diff --git a/Documentation/dev-cla.txt b/Documentation/dev-cla.txt
index 3311d49..267351f 100644
--- a/Documentation/dev-cla.txt
+++ b/Documentation/dev-cla.txt
@@ -13,10 +13,10 @@
   tab on the settings page
 . Click on 'New Contributor Agreement' and follow the instructions
 
-For reference, the actual agreements are linked below
+For reference, the actual agreements are linked below:
 
-* link:https://cla.developers.google.com/about/android-individual[Individual Agreement]
-* link:https://source.android.com/source/cla-corporate.pdf[Corporate Agreement]
+* link:https://cla.developers.google.com/about/google-individual[Individual Agreement]
+* link:https://cla.developers.google.com/about/google-corporate[Corporate Agreement]
 
 GERRIT
 ------
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 6e61e29..17c1184 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -52,6 +52,7 @@
 * commons:pool
 * commons:validator
 * dropwizard:dropwizard-core
+* errorprone:annotations
 * flogger:api
 * fonts:robotofonts
 * guice:guice
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index fd2bef37..c7e21f1 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -1,8 +1,8 @@
 = Gerrit Code Review - NoteDb Backend
 
 NoteDb is the next generation of Gerrit storage backend, which replaces the
-traditional SQL backend for change and account metadata with storing data in the
-same repository as code changes.
+traditional SQL backend for change, account and group metadata with storing
+data in the same repository as code changes.
 
 .Advantages
 - *Simplicity*: All data is stored in one location in the site directory, rather
@@ -32,12 +32,15 @@
   2.15 release. Account data is migrated automatically during the upgrade
   process by running `gerrit.war init`.
 - Storing link:config-groups.html[group metadata] is fully implemented
-  for the 2.16 release. Group data is migrated automatically during
+  in the 2.16 release. Group data is migrated automatically during
   the upgrade process by running `gerrit.war init`
 - Account, group and change metadata on the servers behind `googlesource.com` is fully
   migrated to NoteDb. In other words, if you use
   link:https://gerrit-review.googlesource.com/[gerrit-review], you're already
   using NoteDb.
+- NoteDb is the only database format supported by Gerrit 3.0. The offline
+  change data migration tool is included in Gerrit 3.0, but online
+  migration is only available in the 2.x line.
 
 For an example NoteDb change, poke around at this one:
 ----
@@ -45,12 +48,6 @@
       && git log -p FETCH_HEAD
 ----
 
-== Future Work ("Gerrit 3.0")
-
-- NoteDb will be the only database format supported by Gerrit 3.0. The offline
-  change data migration tool will be included in Gerrit 3.0, but online
-  migration will only be available in the 2.x line.
-
 [[migration]]
 == Migration
 
@@ -64,6 +61,9 @@
 [[online-migration]]
 === Online
 
+Note that online migration is only available in 2.x. To do the online migration
+from 2.14.x or 2.15.x to 3.0, it is necessary to first upgrade to 2.16.x.
+
 To start the online migration, set the `noteDb.changes.autoMigrate` option in
 `gerrit.config` and restart Gerrit:
 ----
@@ -87,7 +87,7 @@
 
 *Disadvantages*
 
-* Only available in 2.x; will not be available in Gerrit 3.0.
+* Only available in 2.x; not available in Gerrit 3.0.
 * Much slower than offline; uses only a single thread, to leave resources
   available for serving traffic.
 * Performance may be degraded, particularly of updates; data needs to be written
diff --git a/WORKSPACE b/WORKSPACE
index f2c387b..7aa09e2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -186,6 +186,12 @@
     sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
 )
 
+maven_jar(
+    name = "error-prone-annotations",
+    artifact = "com.google.errorprone:error_prone_annotations:2.3.3",
+    sha1 = "42aa5155a54a87d70af32d4b0d06bf43779de0e2",
+)
+
 FLOGGER_VERS = "0.4"
 
 maven_jar(
@@ -1065,8 +1071,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.0.1",
-    sha1 = "bc8c679f6e53a51a99190a7a3108ab760b24bbf5",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.1.0",
+    sha1 = "93e8e8b96121069d1d6a6f94d29e7aebd3327301",
 )
 
 maven_jar(
@@ -1075,18 +1081,18 @@
     sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
 )
 
-TESTCONTAINERS_VERSION = "1.11.2"
+TESTCONTAINERS_VERSION = "1.11.3"
 
 maven_jar(
     name = "testcontainers",
     artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-    sha1 = "eae47ed24bb07270d4b60b5e2c3444c5bf3c8ea9",
+    sha1 = "154b69dd976416734b2fc809fb86e173ad9aa25b",
 )
 
 maven_jar(
     name = "testcontainers-elasticsearch",
     artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-    sha1 = "a327bd8cb68eb7146b36d754aee98a8018132d8f",
+    sha1 = "90713b61f5748d8894c31a20f955bd7f81ac2ece",
 )
 
 maven_jar(
@@ -1289,8 +1295,8 @@
 bower_archive(
     name = "polymer-resin",
     package = "polymer/polymer-resin",
-    sha1 = "5cb65081d461e710252a1ba1e671fe4c290356ef",
-    version = "1.2.8",
+    sha1 = "94c29926c20ea3a9b636f26b3e0d689ead8137e5",
+    version = "2.0.1",
 )
 
 bower_archive(
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 043e142..fc9adb8 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -24,9 +24,8 @@
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
@@ -38,13 +37,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Streams;
 import com.google.common.jimfs.Jimfs;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
@@ -125,7 +121,6 @@
 import com.google.gerrit.server.plugins.TestServerPlugin;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.change.Revisions;
@@ -161,7 +156,6 @@
 import java.util.Optional;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -277,9 +271,6 @@
   protected boolean testRequiresSsh;
   protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
 
-  // TODO(dborowitz): Push down into callers that need it.
-  @Inject protected ProjectOperations projectOperations;
-
   @Inject private AbstractChangeNotes.Args changeNotesArgs;
   @Inject private AccountIndexCollection accountIndexes;
   @Inject private AccountIndexer accountIndexer;
@@ -882,60 +873,6 @@
     return gApi.changes().id(r.getChangeId()).current();
   }
 
-  protected void allow(String ref, String permission, AccountGroup.UUID id) throws Exception {
-    allow(project, ref, permission, id);
-  }
-
-  protected void allow(Project.NameKey p, String ref, String permission, AccountGroup.UUID id) {
-    projectOperations
-        .project(p)
-        .forUpdate()
-        .add(TestProjectUpdate.allow(permission).ref(ref).group(id))
-        .update();
-  }
-
-  protected void allowGlobalCapabilities(
-      AccountGroup.UUID id, int min, int max, String... capabilityNames) throws Exception {
-    // TODO(dborowitz): When inlining:
-    // * add a variant that takes a single String
-    // * explicitly add multiple values in callers instead of looping
-    TestProjectUpdate.Builder b = projectOperations.project(allProjects).forUpdate();
-    Arrays.stream(capabilityNames)
-        .forEach(c -> b.add(TestProjectUpdate.allowCapability(c).group(id).range(min, max)));
-    b.update();
-  }
-
-  protected void allowGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
-      throws Exception {
-    allowGlobalCapabilities(id, Arrays.asList(capabilityNames));
-  }
-
-  protected void allowGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
-      throws Exception {
-    // TODO(dborowitz): When inlining:
-    // * add a variant that takes a single String
-    // * explicitly add multiple values in callers instead of looping
-    TestProjectUpdate.Builder b = projectOperations.project(allProjects).forUpdate();
-    Streams.stream(capabilityNames)
-        .forEach(c -> b.add(TestProjectUpdate.allowCapability(c).group(id)));
-    b.update();
-  }
-
-  protected void removeGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
-      throws Exception {
-    removeGlobalCapabilities(id, Arrays.asList(capabilityNames));
-  }
-
-  protected void removeGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.remove(u.getConfig(), capabilityName, id);
-      }
-      u.save();
-    }
-  }
-
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = projectConfigFactory.read(md);
@@ -954,108 +891,6 @@
     }
   }
 
-  protected void deny(String ref, String permission, AccountGroup.UUID id) throws Exception {
-    deny(project, ref, permission, id);
-  }
-
-  protected void deny(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    projectOperations
-        .project(p)
-        .forUpdate()
-        .add(TestProjectUpdate.deny(permission).ref(ref).group(id))
-        .update();
-  }
-
-  protected void block(String ref, String permission, AccountGroup.UUID id) throws Exception {
-    block(project, ref, permission, id);
-  }
-
-  protected void block(Project.NameKey project, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(TestProjectUpdate.block(permission).ref(ref).group(id))
-        .update();
-  }
-
-  protected void blockLabel(
-      String label, int min, int max, AccountGroup.UUID id, String ref, Project.NameKey project)
-      throws Exception {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(TestProjectUpdate.blockLabel(label).ref(ref).group(id).range(min, max))
-        .update();
-  }
-
-  protected void grant(Project.NameKey project, String ref, String permission) {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(TestProjectUpdate.allow(permission).ref(ref).group(adminGroupUuid()))
-        .update();
-  }
-
-  protected void grant(Project.NameKey project, String ref, String permission, boolean force) {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(TestProjectUpdate.allow(permission).ref(ref).group(adminGroupUuid()).force(force))
-        .update();
-  }
-
-  protected void grant(
-      Project.NameKey project,
-      String ref,
-      String permission,
-      boolean force,
-      AccountGroup.UUID groupUUID) {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(TestProjectUpdate.allow(permission).ref(ref).group(groupUUID).force(force))
-        .update();
-  }
-
-  protected void grantLabel(
-      String label,
-      int min,
-      int max,
-      Project.NameKey project,
-      String ref,
-      AccountGroup.UUID groupUUID,
-      boolean exclusive) {
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(
-            TestProjectUpdate.allowLabel(label)
-                .ref(ref)
-                .group(groupUUID)
-                .range(min, max)
-                .exclusive(exclusive))
-        .update();
-  }
-
-  protected void removePermission(Project.NameKey project, String ref, String permission)
-      throws IOException, ConfigInvalidException {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Remove %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      p.clearRules();
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
-  protected void blockRead(String ref) throws Exception {
-    block(ref, Permission.READ, REGISTERED_USERS);
-  }
-
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
@@ -1120,15 +955,6 @@
     }
   }
 
-  // TODO(hanwen): push this down.
-  protected RevCommit getRemoteHead(Project.NameKey project, String branch) throws Exception {
-    return projectOperations.project(project).getHead(branch);
-  }
-
-  protected RevCommit getRemoteHead() throws Exception {
-    return getRemoteHead(project, "master");
-  }
-
   protected void assertMailReplyTo(Message message, String email) throws Exception {
     assertThat(message.headers()).containsKey("Reply-To");
     EmailHeader.String replyTo = (EmailHeader.String) message.headers().get("Reply-To");
@@ -1500,7 +1326,7 @@
       LabelValue... value)
       throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType labelType = category(label, value);
+      LabelType labelType = label(label, value);
       labelType.setFunction(func);
       labelType.setRefPatterns(refPatterns);
       u.getConfig().getLabelSections().put(labelType.getName(), labelType);
@@ -1508,6 +1334,10 @@
     }
   }
 
+  /**
+   * @deprecated Use {@code assert_().fail()} from {@link com.google.common.truth.Truth} instead.
+   */
+  @Deprecated
   protected void fail(@Nullable String format, Object... args) {
     assert_().fail(format, args);
   }
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index d7bcce2..c6f9d32 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -73,7 +73,11 @@
   }
 
   protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
-    return assertAbout(FakeEmailSenderSubject::new).that(sender);
+    return assertAbout(fakeEmailSenders()).that(sender);
+  }
+
+  protected static Subject.Factory<FakeEmailSenderSubject, FakeEmailSender> fakeEmailSenders() {
+    return FakeEmailSenderSubject::new;
   }
 
   protected void setEmailStrategy(TestAccount account, EmailStrategy strategy) throws Exception {
@@ -142,9 +146,7 @@
                     : header));
       }
 
-      // Return a named subject that displays a human-readable table of
-      // recipients.
-      return named(recipientMapToString(recipients, users::emailToName));
+      return this;
     }
 
     private static String recipientMapToString(
@@ -205,8 +207,9 @@
       if (recipients.get(type).contains(email) != expected) {
         failWithoutActual(
             fact(
-                expected ? "should notify" : "shouldn't notify",
-                type + ": " + users.emailToName(email)));
+                expected ? "expected to notify" : "expected not to notify",
+                type + ": " + users.emailToName(email)),
+            fact("but notified", recipientMapToString(recipients, users::emailToName)));
       }
       if (expected) {
         accountedFor.add(email);
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index ada2fb6..0da6287 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -1,6 +1,11 @@
 load("//tools/bzl:java.bzl", "java_library2")
 load("//tools/bzl:javadoc.bzl", "java_doc")
 
+FUNCTION_SRCS = [
+    "testsuite/ThrowingConsumer.java",
+    "testsuite/ThrowingFunction.java",
+]
+
 java_library(
     name = "lib",
     testonly = True,
@@ -68,8 +73,13 @@
 java_library2(
     name = "framework-lib",
     testonly = True,
-    srcs = glob(["**/*.java"]),
+    srcs = glob(
+        ["**/*.java"],
+        exclude = FUNCTION_SRCS,
+    ),
     exported_deps = [
+        ":function",
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/gpg",
         "//java/com/google/gerrit/httpd/auth/openid",
@@ -134,6 +144,12 @@
     ],
 )
 
+java_library(
+    name = "function",
+    srcs = FUNCTION_SRCS,
+    visibility = ["//visibility:public"],
+)
+
 java_doc(
     name = "framework-javadoc",
     testonly = True,
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 3a49f46..a48a278 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -524,7 +524,7 @@
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
-    Injector sysInjector = get(daemon, "sysInjector");
+    Injector sysInjector = getInjector(daemon, "sysInjector");
     Module module =
         new FactoryModule() {
           @Override
@@ -557,13 +557,14 @@
     return sysInjector.createChildInjector(module);
   }
 
-  @SuppressWarnings("unchecked")
-  private static <T> T get(Object obj, String field)
+  private static Injector getInjector(Object obj, String field)
       throws SecurityException, NoSuchFieldException, IllegalArgumentException,
           IllegalAccessException {
     Field f = obj.getClass().getDeclaredField(field);
     f.setAccessible(true);
-    return (T) f.get(obj);
+    Object v = f.get(obj);
+    checkArgument(v instanceof Injector, "not an Injector: %s", v);
+    return (Injector) f.get(obj);
   }
 
   private static InetAddress getLocalHost() {
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index 7d5bcab..bb1c123 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -110,16 +110,12 @@
       throws Exception {
     DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());
 
-    FS fs = FS.detect();
-
-    // Avoid leaking user state into our tests.
-    fs.setUserHome(null);
-
     InMemoryRepository.Builder b = new InMemoryRepository.Builder().setRepositoryDescription(desc);
     if (uri.startsWith("ssh://")) {
       // SshTransport depends on a real FS to read ~/.ssh/config, but InMemoryRepository by default
       // uses a null FS.
-      b.setFS(fs);
+      // Avoid leaking user state into our tests.
+      b.setFS(FS.detect().setUserHome(null));
     }
     InMemoryRepository dest = b.build();
     Config cfg = dest.getConfig();
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
new file mode 100644
index 0000000..3215a9c
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -0,0 +1,21 @@
+package(default_testonly = 1)
+
+java_library(
+    name = "project",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/acceptance:function",
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/project/testing:project-test-util",
+        "//lib:guava",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/commons:lang",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
index fc4caf8..b310393 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
@@ -30,6 +30,9 @@
 
   PerProjectOperations project(Project.NameKey key);
 
+  /** Starts a fluent chain for updating All-Projects. */
+  TestProjectUpdate.Builder allProjectsForUpdate();
+
   interface PerProjectOperations {
     /**
      * Returns the commit for this project. branchName can either be shortened ("HEAD", "master") or
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 6835ae4..34e57b4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -20,22 +20,25 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectCreation.Builder;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCreator;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -53,6 +56,7 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 
 public class ProjectOperationsImpl implements ProjectOperations {
+  private final AllProjectsName allProjectsName;
   private final GitRepositoryManager repoManager;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
   private final ProjectCache projectCache;
@@ -61,11 +65,13 @@
 
   @Inject
   ProjectOperationsImpl(
+      AllProjectsName allProjectsName,
       GitRepositoryManager repoManager,
       MetaDataUpdate.Server metaDataUpdateFactory,
       ProjectCache projectCache,
       ProjectConfig.Factory projectConfigFactory,
       ProjectCreator projectCreator) {
+    this.allProjectsName = allProjectsName;
     this.repoManager = repoManager;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
@@ -98,6 +104,11 @@
     return new PerProjectOperations(key);
   }
 
+  @Override
+  public TestProjectUpdate.Builder allProjectsForUpdate() {
+    return project(allProjectsName).forUpdate();
+  }
+
   private class PerProjectOperations implements ProjectOperations.PerProjectOperations {
     Project.NameKey nameKey;
 
@@ -117,25 +128,43 @@
 
     @Override
     public TestProjectUpdate.Builder forUpdate() {
-      return TestProjectUpdate.builder(this::updateProject);
+      return TestProjectUpdate.builder(nameKey, allProjectsName, this::updateProject);
     }
 
     private void updateProject(TestProjectUpdate projectUpdate)
         throws IOException, ConfigInvalidException {
       try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) {
         ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate);
+        removePermissions(projectConfig, projectUpdate.removedPermissions());
         addCapabilities(projectConfig, projectUpdate.addedCapabilities());
         addPermissions(projectConfig, projectUpdate.addedPermissions());
         addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
+        setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions());
         projectConfig.commit(metaDataUpdate);
       }
       projectCache.evict(nameKey);
     }
 
+    private void removePermissions(
+        ProjectConfig projectConfig,
+        ImmutableList<TestProjectUpdate.TestPermissionKey> removedPermissions) {
+      for (TestProjectUpdate.TestPermissionKey p : removedPermissions) {
+        Permission permission =
+            projectConfig.getAccessSection(p.section(), true).getPermission(p.name(), true);
+        if (p.group().isPresent()) {
+          GroupReference group = new GroupReference(p.group().get(), p.group().get().get());
+          group = projectConfig.resolve(group);
+          permission.removeRule(group);
+        } else {
+          permission.clearRules();
+        }
+      }
+    }
+
     private void addCapabilities(
         ProjectConfig projectConfig, ImmutableList<TestCapability> addedCapabilities) {
       for (TestCapability c : addedCapabilities) {
-        PermissionRule rule = Util.newRule(projectConfig, c.group());
+        PermissionRule rule = newRule(projectConfig, c.group());
         rule.setRange(c.min(), c.max());
         projectConfig
             .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
@@ -147,7 +176,7 @@
     private void addPermissions(
         ProjectConfig projectConfig, ImmutableList<TestPermission> addedPermissions) {
       for (TestPermission p : addedPermissions) {
-        PermissionRule rule = Util.newRule(projectConfig, p.group());
+        PermissionRule rule = newRule(projectConfig, p.group());
         rule.setAction(p.action());
         rule.setForce(p.force());
         projectConfig.getAccessSection(p.ref(), true).getPermission(p.name(), true).add(rule);
@@ -157,18 +186,28 @@
     private void addLabelPermissions(
         ProjectConfig projectConfig, ImmutableList<TestLabelPermission> addedLabelPermissions) {
       for (TestLabelPermission p : addedLabelPermissions) {
-        PermissionRule rule = Util.newRule(projectConfig, p.group());
+        PermissionRule rule = newRule(projectConfig, p.group());
         rule.setAction(p.action());
         rule.setRange(p.min(), p.max());
+        String permissionName =
+            p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
         Permission permission =
-            projectConfig
-                .getAccessSection(p.ref(), true)
-                .getPermission(Permission.forLabel(p.name()), true);
-        permission.setExclusiveGroup(p.exclusive());
+            projectConfig.getAccessSection(p.ref(), true).getPermission(permissionName, true);
         permission.add(rule);
       }
     }
 
+    private void setExclusiveGroupPermissions(
+        ProjectConfig projectConfig,
+        ImmutableMap<TestProjectUpdate.TestPermissionKey, Boolean> exclusiveGroupPermissions) {
+      exclusiveGroupPermissions.forEach(
+          (key, exclusive) ->
+              projectConfig
+                  .getAccessSection(key.section(), true)
+                  .getPermission(key.name(), true)
+                  .setExclusiveGroup(exclusive));
+    }
+
     private RevCommit headOrNull(String branch) {
       if (!branch.startsWith(Constants.R_REFS)) {
         branch = RefNames.REFS_HEADS + branch;
@@ -217,4 +256,10 @@
       }
     }
   }
+
+  private static PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+    return new PermissionRule(group);
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index b58eae6..6cbf40d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.common.data.AccessSection.GLOBAL_CAPABILITIES;
+import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
@@ -25,7 +28,10 @@
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
 
 @AutoValue
 public abstract class TestProjectUpdate {
@@ -70,6 +76,7 @@
 
       /** Sets the minimum and maximum values for the capability. */
       public Builder range(int min, int max) {
+        checkNonInvertedRange(min, max);
         return min(min).max(max);
       }
 
@@ -77,17 +84,20 @@
       abstract TestCapability autoBuild();
 
       public TestCapability build() {
-        if (min().isPresent() || max().isPresent()) {
-          checkArgument(
-              GlobalCapability.hasRange(name()), "capability %s does not support ranges", name());
-        }
         PermissionRange.WithDefaults withDefaults = GlobalCapability.getRange(name());
-        if (!min().isPresent()) {
-          min(withDefaults != null ? withDefaults.getDefaultMin() : 0);
+        if (withDefaults != null) {
+          int min = min().orElse(withDefaults.getDefaultMin());
+          int max = max().orElse(withDefaults.getDefaultMax());
+          range(min, max);
+          // Don't enforce range is nonempty; this is allowed for e.g. batchChangesLimit.
+        } else {
+          checkArgument(
+              !min().isPresent() && !max().isPresent(),
+              "capability %s does not support ranges",
+              name());
+          range(0, 0);
         }
-        if (!max().isPresent()) {
-          max(withDefaults != null ? withDefaults.getDefaultMax() : 0);
-        }
+
         return autoBuild();
       }
     }
@@ -164,7 +174,7 @@
   @AutoValue
   public abstract static class TestLabelPermission {
     private static Builder builder() {
-      return new AutoValue_TestProjectUpdate_TestLabelPermission.Builder().exclusive(false);
+      return new AutoValue_TestProjectUpdate_TestLabelPermission.Builder().impersonation(false);
     }
 
     abstract String name();
@@ -179,7 +189,7 @@
 
     abstract int max();
 
-    abstract boolean exclusive();
+    abstract boolean impersonation();
 
     /** Builder for {@link TestLabelPermission}. */
     @AutoValue.Builder
@@ -200,40 +210,110 @@
 
       /** Sets the minimum and maximum values for the permission. */
       public Builder range(int min, int max) {
+        checkArgument(min != 0 || max != 0, "empty range");
+        checkNonInvertedRange(min, max);
         return min(min).max(max);
       }
 
-      /** Adds the permission to the exclusive group permission set on the access section. */
-      public abstract Builder exclusive(boolean exclusive);
+      /** Sets whether this permission should be for impersonating another user's votes. */
+      public abstract Builder impersonation(boolean impersonation);
 
       abstract TestLabelPermission autoBuild();
 
       /** Builds the {@link TestPermission}. */
       public TestLabelPermission build() {
         TestLabelPermission result = autoBuild();
-        checkArgument(
-            !Permission.isLabel(result.name()),
-            "expected label name, got permission name: %s",
-            result.name());
-        LabelType.checkName(result.name());
+        checkLabelName(result.name());
         return result;
       }
     }
   }
 
-  static Builder builder(ThrowingConsumer<TestProjectUpdate> projectUpdater) {
-    return new AutoValue_TestProjectUpdate.Builder().projectUpdater(projectUpdater);
+  /**
+   * Starts a builder for describing a permission key for deletion. Not for label permissions or
+   * global capabilities.
+   */
+  public static TestPermissionKey.Builder permissionKey(String name) {
+    return TestPermissionKey.builder().name(name);
+  }
+
+  /** Starts a builder for describing a label permission key for deletion. */
+  public static TestPermissionKey.Builder labelPermissionKey(String name) {
+    checkLabelName(name);
+    return TestPermissionKey.builder().name(Permission.forLabel(name));
+  }
+
+  /** Starts a builder for describing a capability key for deletion. */
+  public static TestPermissionKey.Builder capabilityKey(String name) {
+    return TestPermissionKey.builder().name(name).section(GLOBAL_CAPABILITIES);
+  }
+
+  /** Records the key of a permission (of any type) for deletion. */
+  @AutoValue
+  public abstract static class TestPermissionKey {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestPermissionKey.Builder();
+    }
+
+    abstract String section();
+
+    abstract String name();
+
+    abstract Optional<AccountGroup.UUID> group();
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder section(String section);
+
+      abstract Optional<String> section();
+
+      /** Sets the ref pattern used on the permission. Not for global capabilities. */
+      public Builder ref(String ref) {
+        requireNonNull(ref);
+        checkArgument(ref.startsWith(Constants.R_REFS), "must be a ref: %s", ref);
+        checkArgument(
+            !section().isPresent() || !section().get().equals(GLOBAL_CAPABILITIES),
+            "can't set ref on global capability");
+        return section(ref);
+      }
+
+      abstract Builder name(String name);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      /** Builds the {@link TestPermissionKey}. */
+      public abstract TestPermissionKey build();
+    }
+  }
+
+  static Builder builder(
+      Project.NameKey nameKey,
+      AllProjectsName allProjectsName,
+      ThrowingConsumer<TestProjectUpdate> projectUpdater) {
+    return new AutoValue_TestProjectUpdate.Builder()
+        .nameKey(nameKey)
+        .allProjectsName(allProjectsName)
+        .projectUpdater(projectUpdater);
   }
 
   /** Builder for {@link TestProjectUpdate}. */
   @AutoValue.Builder
   public abstract static class Builder {
+    abstract Builder nameKey(Project.NameKey project);
+
+    abstract Builder allProjectsName(AllProjectsName allProjects);
+
     abstract ImmutableList.Builder<TestPermission> addedPermissionsBuilder();
 
     abstract ImmutableList.Builder<TestLabelPermission> addedLabelPermissionsBuilder();
 
     abstract ImmutableList.Builder<TestCapability> addedCapabilitiesBuilder();
 
+    abstract ImmutableList.Builder<TestPermissionKey> removedPermissionsBuilder();
+
+    abstract ImmutableMap.Builder<TestPermissionKey, Boolean> exclusiveGroupPermissionsBuilder();
+
     /** Adds a permission to be included in this update. */
     public Builder add(TestPermission testPermission) {
       addedPermissionsBuilder().add(testPermission);
@@ -267,22 +347,90 @@
       return add(testCapabilityBuilder.build());
     }
 
+    /** Removes a permission, label permission, or capability as part of this update. */
+    public Builder remove(TestPermissionKey testPermissionKey) {
+      removedPermissionsBuilder().add(testPermissionKey);
+      return this;
+    }
+
+    /** Removes a permission, label permission, or capability as part of this update. */
+    public Builder remove(TestPermissionKey.Builder testPermissionKeyBuilder) {
+      return remove(testPermissionKeyBuilder.build());
+    }
+
+    /** Sets the exclusive bit bit for the given permission key. */
+    public Builder setExclusiveGroup(
+        TestPermissionKey.Builder testPermissionKeyBuilder, boolean exclusive) {
+      return setExclusiveGroup(testPermissionKeyBuilder.build(), exclusive);
+    }
+
+    /** Sets the exclusive bit bit for the given permission key. */
+    public Builder setExclusiveGroup(TestPermissionKey testPermissionKey, boolean exclusive) {
+      checkArgument(
+          !testPermissionKey.group().isPresent(),
+          "do not specify group for setExclusiveGroup: %s",
+          testPermissionKey);
+      checkArgument(
+          !testPermissionKey.section().equals(GLOBAL_CAPABILITIES),
+          "setExclusiveGroup not valid for global capabilities: %s",
+          testPermissionKey);
+      exclusiveGroupPermissionsBuilder().put(testPermissionKey, exclusive);
+      return this;
+    }
+
     abstract Builder projectUpdater(ThrowingConsumer<TestProjectUpdate> projectUpdater);
 
     abstract TestProjectUpdate autoBuild();
 
+    TestProjectUpdate build() {
+      TestProjectUpdate projectUpdate = autoBuild();
+      if (projectUpdate.hasCapabilityUpdates()) {
+        checkArgument(
+            projectUpdate.nameKey().equals(projectUpdate.allProjectsName()),
+            "cannot update global capabilities on %s, only %s: %s",
+            projectUpdate.nameKey(),
+            projectUpdate.allProjectsName(),
+            projectUpdate);
+      }
+      return projectUpdate;
+    }
+
     /** Executes the update, updating the underlying project. */
     public void update() {
-      TestProjectUpdate projectUpdate = autoBuild();
+      TestProjectUpdate projectUpdate = build();
       projectUpdate.projectUpdater().acceptAndThrowSilently(projectUpdate);
     }
   }
 
+  abstract Project.NameKey nameKey();
+
+  abstract AllProjectsName allProjectsName();
+
   abstract ImmutableList<TestPermission> addedPermissions();
 
   abstract ImmutableList<TestLabelPermission> addedLabelPermissions();
 
   abstract ImmutableList<TestCapability> addedCapabilities();
 
+  abstract ImmutableList<TestPermissionKey> removedPermissions();
+
+  abstract ImmutableMap<TestPermissionKey, Boolean> exclusiveGroupPermissions();
+
   abstract ThrowingConsumer<TestProjectUpdate> projectUpdater();
+
+  boolean hasCapabilityUpdates() {
+    return !addedCapabilities().isEmpty()
+        || removedPermissions().stream().anyMatch(k -> k.section().equals(GLOBAL_CAPABILITIES));
+  }
+
+  private static void checkLabelName(String name) {
+    // "label-Code-Review" is technically a valid label name, and we don't prevent users from
+    // using it in production, but specifying it in a test is programmer error.
+    checkArgument(!Permission.isLabel(name), "expected label name, got permission name: %s", name);
+    LabelType.checkName(name);
+  }
+
+  private static void checkNonInvertedRange(int min, int max) {
+    checkArgument(min <= max, "inverted range: %s > %s", min, max);
+  }
 }
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 1be6353..1816d50 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -22,13 +23,13 @@
 import java.lang.annotation.Target;
 
 /**
- * A marker for a method that is public solely because it is called from inside a project or an
- * organisation using Gerrit.
+ * A marker to say a method/type/field is added or is increased to public solely because it is
+ * called from inside a project or an organisation using Gerrit.
  */
-@Target({METHOD, TYPE})
+@Target({METHOD, TYPE, FIELD})
 @Retention(RUNTIME)
 public @interface UsedAt {
-  /** Enumeration of projects that call a method that would otherwise be private. */
+  /** Enumeration of projects that call a method/type/field. */
   enum Project {
     GOOGLE,
     PLUGIN_CHECKS,
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 86f1083..41cec1e 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -185,7 +185,7 @@
   @Override
   public void deleteAll() {
     // Delete the index, if it exists.
-    String endpoint = indexName + client.adapter().indicesExistParam();
+    String endpoint = indexName + client.adapter().indicesExistParams();
     Response response = performRequest("HEAD", endpoint);
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode == HttpStatus.SC_OK) {
@@ -224,10 +224,15 @@
   }
 
   protected String getMappingsFor(String type, MappingProperties properties) {
-    JsonObject mappingType = new JsonObject();
-    mappingType.add(type, gson.toJsonTree(properties));
     JsonObject mappings = new JsonObject();
-    mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
+
+    if (client.adapter().omitType()) {
+      mappings.add(MAPPINGS, gson.toJsonTree(properties));
+    } else {
+      JsonObject mappingType = new JsonObject();
+      mappingType.add(type, gson.toJsonTree(properties));
+      mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
+    }
     return gson.toJson(mappings);
   }
 
@@ -310,11 +315,12 @@
   protected String getURI(String type, String request) {
     try {
       String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
-      if (SEARCH.equals(request) && client.adapter().omitTypeFromSearch()) {
+      if (SEARCH.equals(request) && client.adapter().omitType()) {
         return encodedIndexName + "/" + request;
       }
-      String encodedType = URLEncoder.encode(type, UTF_8.toString());
-      return encodedIndexName + "/" + encodedType + "/" + request;
+      String encodedTypeIfAny =
+          client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
+      return encodedIndexName + encodedTypeIfAny + "/" + request;
     } catch (UnsupportedEncodingException e) {
       throw new StorageException(e);
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 38f8578..e8a168f 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -122,7 +122,7 @@
     BulkRequest bulk =
         new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter)
             .add(new UpdateRequest<>(schema, cd));
-    if (!adapter.usePostV5Type()) {
+    if (adapter.deleteToReplace()) {
       bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter));
     }
 
@@ -141,17 +141,19 @@
       throws QueryParseException {
     Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
     List<String> indexes = Lists.newArrayListWithCapacity(2);
-    if (client.adapter().usePostV5Type()) {
-      if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
-          || !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-        indexes.add(ElasticQueryAdapter.POST_V5_TYPE);
-      }
-    } else {
-      if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
-        indexes.add(OPEN_CHANGES);
-      }
-      if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-        indexes.add(CLOSED_CHANGES);
+    if (!client.adapter().omitType()) {
+      if (client.adapter().useV6Type()) {
+        if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
+            || !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+          indexes.add(ElasticQueryAdapter.V6_TYPE);
+        }
+      } else {
+        if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+          indexes.add(OPEN_CHANGES);
+        }
+        if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+          indexes.add(CLOSED_CHANGES);
+        }
       }
     }
 
@@ -176,16 +178,16 @@
 
   @Override
   protected String getDeleteActions(Change.Id c) {
-    if (client.adapter().usePostV5Type()) {
-      return delete(ElasticQueryAdapter.POST_V5_TYPE, c);
+    if (!client.adapter().useV5Type()) {
+      return delete(client.adapter().getType(), c);
     }
     return delete(OPEN_CHANGES, c) + delete(CLOSED_CHANGES, c);
   }
 
   @Override
   protected String getMappings() {
-    if (client.adapter().usePostV5Type()) {
-      return getMappingsFor(ElasticQueryAdapter.POST_V5_TYPE, mapping.changes);
+    if (!client.adapter().useV5Type()) {
+      return getMappingsFor(client.adapter().getType(), mapping.changes);
     }
     return gson.toJson(ImmutableMap.of(MAPPINGS, mapping));
   }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 23b6ffd..e34644e 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -17,14 +17,18 @@
 import com.google.gson.JsonObject;
 
 public class ElasticQueryAdapter {
-  static final String POST_V5_TYPE = "_doc";
+  static final String V6_TYPE = "_doc";
+
+  private static final String INCLUDE_TYPE = "include_type_name=true";
+  private static final String INDICES = "?allow_no_indices=false";
 
   private final boolean ignoreUnmapped;
-  private final boolean usePostV5Type;
-  private final boolean omitTypeFromSearch;
+  private final boolean useV5Type;
+  private final boolean useV6Type;
+  private final boolean omitType;
 
   private final String searchFilteringName;
-  private final String indicesExistParam;
+  private final String indicesExistParams;
   private final String exactFieldType;
   private final String stringFieldType;
   private final String indexProperty;
@@ -34,16 +38,17 @@
 
   ElasticQueryAdapter(ElasticVersion version) {
     this.ignoreUnmapped = false;
-    this.usePostV5Type = version.isV6OrLater();
-    this.omitTypeFromSearch = version.isV7OrLater();
+    this.useV5Type = !version.isV6OrLater();
+    this.useV6Type = version.isV6();
+    this.omitType = version.isV7OrLater();
     this.versionDiscoveryUrl = version.isV6OrLater() ? "/%s*" : "/%s*/_aliases";
     this.searchFilteringName = "_source";
-    this.indicesExistParam = "?allow_no_indices=false";
+    this.indicesExistParams = version.isV6() ? INDICES + "&" + INCLUDE_TYPE : INDICES;
     this.exactFieldType = "keyword";
     this.stringFieldType = "text";
     this.indexProperty = "true";
     this.rawFieldsKey = "_source";
-    this.includeTypeNameParam = version.isV7OrLater() ? "?include_type_name=true" : "";
+    this.includeTypeNameParam = version.isV6() ? "?" + INCLUDE_TYPE : "";
   }
 
   void setIgnoreUnmapped(JsonObject properties) {
@@ -53,7 +58,7 @@
   }
 
   public void setType(JsonObject properties, String type) {
-    if (!usePostV5Type) {
+    if (useV5Type) {
       properties.addProperty("_type", type);
     }
   }
@@ -62,8 +67,8 @@
     return searchFilteringName;
   }
 
-  String indicesExistParam() {
-    return indicesExistParam;
+  String indicesExistParams() {
+    return indicesExistParams;
   }
 
   String exactFieldType() {
@@ -82,16 +87,31 @@
     return rawFieldsKey;
   }
 
-  boolean usePostV5Type() {
-    return usePostV5Type;
+  boolean deleteToReplace() {
+    return useV5Type;
   }
 
-  boolean omitTypeFromSearch() {
-    return omitTypeFromSearch;
+  boolean useV5Type() {
+    return useV5Type;
+  }
+
+  boolean useV6Type() {
+    return useV6Type;
+  }
+
+  boolean omitType() {
+    return omitType;
+  }
+
+  String getType() {
+    return getType("");
   }
 
   String getType(String type) {
-    return usePostV5Type() ? POST_V5_TYPE : type;
+    if (useV6Type()) {
+      return V6_TYPE;
+    }
+    return useV5Type() ? type : "";
   }
 
   String getVersionDiscoveryUrl(String name) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 6de4d97..98e1f7d 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -25,7 +25,8 @@
   V6_5("6.5.*"),
   V6_6("6.6.*"),
   V6_7("6.7.*"),
-  V7_0("7.0.*");
+  V7_0("7.0.*"),
+  V7_1("7.1.*");
 
   private final String version;
   private final Pattern pattern;
@@ -58,6 +59,10 @@
     return Joiner.on(", ").join(ElasticVersion.values());
   }
 
+  public boolean isV6() {
+    return getMajor() == 6;
+  }
+
   public boolean isV6OrLater() {
     return isAtLeastVersion(6);
   }
@@ -67,7 +72,11 @@
   }
 
   private boolean isAtLeastVersion(int v) {
-    return Integer.valueOf(version.split("\\.")[0]) >= v;
+    return getMajor() >= v;
+  }
+
+  private Integer getMajor() {
+    return Integer.valueOf(version.split("\\.")[0]);
   }
 
   @Override
diff --git a/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index e676828..75cf713 100644
--- a/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -34,9 +34,8 @@
     super("Not found: " + id.get());
   }
 
-  @SuppressWarnings("unchecked")
-  @Override
   public ResourceNotFoundException caching(CacheControl c) {
-    return super.caching(c);
+    setCaching(c);
+    return this;
   }
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestApiException.java b/java/com/google/gerrit/extensions/restapi/RestApiException.java
index b09723e..f3d7dec 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.util.Objects.requireNonNull;
+
 /** Root exception type for REST API failures. */
 public class RestApiException extends Exception {
   private static final long serialVersionUID = 1L;
@@ -33,9 +35,7 @@
     return caching;
   }
 
-  @SuppressWarnings("unchecked")
-  public <T extends RestApiException> T caching(CacheControl c) {
-    caching = c;
-    return (T) this;
+  protected void setCaching(CacheControl caching) {
+    this.caching = requireNonNull(caching);
   }
 }
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 69e106c..a1217b3 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -17,7 +17,10 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -30,6 +33,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -42,22 +46,26 @@
 import org.eclipse.jgit.lib.RefUpdate;
 
 public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<PersonIdent> serverIdent;
   private final Provider<PublicKeyStore> storeProvider;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final ExternalIds externalIds;
+  private final DeleteKeySender.Factory deleteKeySenderFactory;
 
   @Inject
   DeleteGpgKey(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<PublicKeyStore> storeProvider,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
-      ExternalIds externalIds) {
+      ExternalIds externalIds,
+      DeleteKeySender.Factory deleteKeySenderFactory) {
     this.serverIdent = serverIdent;
     this.storeProvider = storeProvider;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.externalIds = externalIds;
+    this.deleteKeySenderFactory = deleteKeySenderFactory;
   }
 
   @Override
@@ -90,6 +98,15 @@
       switch (saveResult) {
         case NO_CHANGE:
         case FAST_FORWARD:
+          try {
+            deleteKeySenderFactory
+                .create(rsrc.getUser(), ImmutableList.of(PublicKeyStore.keyToString(key)))
+                .send();
+          } catch (EmailException e) {
+            logger.atSevere().withCause(e).log(
+                "Cannot send GPG key deletion message to %s",
+                rsrc.getUser().getAccount().getPreferredEmail());
+          }
           break;
         case FORCED:
         case IO_FAILURE:
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 9752b54..986d5ff 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Throwables;
@@ -54,6 +55,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.AddKeySender;
+import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
@@ -86,7 +88,8 @@
   private final Provider<CurrentUser> self;
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
-  private final AddKeySender.Factory addKeyFactory;
+  private final AddKeySender.Factory addKeySenderFactory;
+  private final DeleteKeySender.Factory deleteKeySenderFactory;
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -98,7 +101,8 @@
       Provider<CurrentUser> self,
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory,
-      AddKeySender.Factory addKeyFactory,
+      AddKeySender.Factory addKeySenderFactory,
+      DeleteKeySender.Factory deleteKeySenderFactory,
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
@@ -107,7 +111,8 @@
     this.self = self;
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
-    this.addKeyFactory = addKeyFactory;
+    this.addKeySenderFactory = addKeySenderFactory;
+    this.deleteKeySenderFactory = deleteKeySenderFactory;
     this.accountQueryProvider = accountQueryProvider;
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
@@ -248,13 +253,24 @@
         case FORCED:
           if (!addedKeys.isEmpty()) {
             try {
-              addKeyFactory.create(user, addedKeys).send();
+              addKeySenderFactory.create(user, addedKeys).send();
             } catch (EmailException e) {
               logger.atSevere().withCause(e).log(
                   "Cannot send GPG key added message to %s",
                   rsrc.getUser().getAccount().getPreferredEmail());
             }
           }
+          if (!toRemove.isEmpty()) {
+            try {
+              deleteKeySenderFactory
+                  .create(user, toRemove.stream().map(Fingerprint::toString).collect(toList()))
+                  .send();
+            } catch (EmailException e) {
+              logger.atSevere().withCause(e).log(
+                  "Cannot send GPG key deleted message to %s",
+                  user.getAccount().getPreferredEmail());
+            }
+          }
           break;
         case NO_CHANGE:
           break;
diff --git a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
index 397d093..4ae7e5e 100644
--- a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
+++ b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
@@ -67,7 +67,7 @@
     headers.put(name, value);
   }
 
-  @SuppressWarnings("all")
+  @SuppressWarnings({"all", "MissingOverride"})
   // @Override is omitted for backwards compatibility with servlet-api 2.5
   // TODO: Remove @SuppressWarnings and add @Override when Google upgrades
   //       to servlet-api 3.1
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 2a0555e..b0b843f 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -349,13 +349,12 @@
   }
 
   private Module createIndexModule() {
-    switch (indexType) {
-      case LUCENE:
-        return LuceneIndexModule.latestVersion(false);
-      case ELASTICSEARCH:
-        return ElasticIndexModule.latestVersion(false);
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (indexType.isLucene()) {
+      return LuceneIndexModule.latestVersion(false);
+    } else if (indexType.isElasticsearch()) {
+      return ElasticIndexModule.latestVersion(false);
+    } else {
+      throw new IllegalStateException("unsupported index.type = " + indexType);
     }
   }
 
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 7d817d2..538e11b 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -78,48 +78,55 @@
     if (source == null) {
       throw new StorageException("No DataSource: " + this);
     }
-    List<T> r = new ArrayList<>();
-    T last = null;
-    int nextStart = 0;
-    boolean skipped = false;
-    for (T data : buffer(source.read())) {
-      if (!isMatchable() || match(data)) {
-        r.add(data);
-      } else {
-        skipped = true;
-      }
-      last = data;
-      nextStart++;
-    }
 
-    if (skipped && last != null && source instanceof Paginated) {
-      // If our source is a paginated source and we skipped at
-      // least one of its results, we may not have filled the full
-      // limit the caller wants.  Restart the source and continue.
-      //
-      @SuppressWarnings("unchecked")
-      Paginated<T> p = (Paginated<T>) source;
-      while (skipped && r.size() < p.getOptions().limit() + start) {
-        skipped = false;
-        ResultSet<T> next = p.restart(nextStart);
-
-        for (T data : buffer(next)) {
-          if (match(data)) {
-            r.add(data);
-          } else {
-            skipped = true;
+    // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
+    // requested allows the index to run asynchronous queries.
+    ResultSet<T> resultSet = source.read();
+    return new LazyResultSet<>(
+        () -> {
+          List<T> r = new ArrayList<>();
+          T last = null;
+          int nextStart = 0;
+          boolean skipped = false;
+          for (T data : buffer(resultSet)) {
+            if (!isMatchable() || match(data)) {
+              r.add(data);
+            } else {
+              skipped = true;
+            }
+            last = data;
+            nextStart++;
           }
-          nextStart++;
-        }
-      }
-    }
 
-    if (start >= r.size()) {
-      r = ImmutableList.of();
-    } else if (start > 0) {
-      r = ImmutableList.copyOf(r.subList(start, r.size()));
-    }
-    return new ListResultSet<>(r);
+          if (skipped && last != null && source instanceof Paginated) {
+            // If our source is a paginated source and we skipped at
+            // least one of its results, we may not have filled the full
+            // limit the caller wants.  Restart the source and continue.
+            //
+            @SuppressWarnings("unchecked")
+            Paginated<T> p = (Paginated<T>) source;
+            while (skipped && r.size() < p.getOptions().limit() + start) {
+              skipped = false;
+              ResultSet<T> next = p.restart(nextStart);
+
+              for (T data : buffer(next)) {
+                if (match(data)) {
+                  r.add(data);
+                } else {
+                  skipped = true;
+                }
+                nextStart++;
+              }
+            }
+          }
+
+          if (start >= r.size()) {
+            return ImmutableList.of();
+          } else if (start > 0) {
+            return ImmutableList.copyOf(r.subList(start, r.size()));
+          }
+          return ImmutableList.copyOf(r);
+        });
   }
 
   @Override
diff --git a/java/com/google/gerrit/index/query/LazyResultSet.java b/java/com/google/gerrit/index/query/LazyResultSet.java
new file mode 100644
index 0000000..f3fab5f
--- /dev/null
+++ b/java/com/google/gerrit/index/query/LazyResultSet.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index.query;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+/**
+ * Result set that allows for asynchronous execution of the actual query. Callers should dispatch
+ * the query and call the constructor of this class with a supplier that fetches the result and
+ * blocks on it if necessary.
+ *
+ * <p>If the execution is synchronous or the results are known a priori, consider using {@link
+ * ListResultSet}.
+ */
+public class LazyResultSet<T> implements ResultSet<T> {
+  private final Supplier<ImmutableList<T>> resultsCallback;
+
+  private boolean resultsReturned = false;
+
+  public LazyResultSet(Supplier<ImmutableList<T>> r) {
+    resultsCallback = requireNonNull(r, "results can't be null");
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return toList().iterator();
+  }
+
+  @Override
+  public ImmutableList<T> toList() {
+    if (resultsReturned) {
+      throw new IllegalStateException("Results already obtained");
+    }
+    resultsReturned = true;
+    return resultsCallback.get();
+  }
+
+  @Override
+  public void close() {}
+}
diff --git a/java/com/google/gerrit/index/query/ListResultSet.java b/java/com/google/gerrit/index/query/ListResultSet.java
index 4cf48c8..9d7eadf 100644
--- a/java/com/google/gerrit/index/query/ListResultSet.java
+++ b/java/com/google/gerrit/index/query/ListResultSet.java
@@ -14,15 +14,25 @@
 
 package com.google.gerrit.index.query;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.collect.ImmutableList;
 import java.util.Iterator;
 import java.util.List;
 
+/**
+ * Result set for queries that run synchronously or for cases where the result is already known and
+ * we just need to pipe it back through our interfaces.
+ *
+ * <p>If your implementation benefits from asynchronous execution (i.e. dispatching a query and
+ * awaiting results only when {@link ResultSet#toList()} is called, consider using {@link
+ * LazyResultSet}.
+ */
 public class ListResultSet<T> implements ResultSet<T> {
-  private ImmutableList<T> items;
+  private ImmutableList<T> results;
 
   public ListResultSet(List<T> r) {
-    items = ImmutableList.copyOf(r);
+    results = ImmutableList.copyOf(requireNonNull(r, "results can't be null"));
   }
 
   @Override
@@ -32,16 +42,16 @@
 
   @Override
   public ImmutableList<T> toList() {
-    if (items == null) {
+    if (results == null) {
       throw new IllegalStateException("Results already obtained");
     }
-    ImmutableList<T> r = items;
-    items = null;
+    ImmutableList<T> r = results;
+    results = null;
     return r;
   }
 
   @Override
   public void close() {
-    items = null;
+    results = null;
   }
 }
diff --git a/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index ba8d7da..4044b90 100644
--- a/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -177,7 +177,7 @@
    * Expert: creates a searcher from the provided {@link IndexReader} using the provided {@link
    * SearcherFactory}. NOTE: this decRefs incoming reader on throwing an exception.
    */
-  @SuppressWarnings("resource")
+  @SuppressWarnings({"resource", "ReferenceEquality"})
   public static IndexSearcher getSearcher(SearcherFactory searcherFactory, IndexReader reader)
       throws IOException {
     boolean success = false;
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 50005f2..ab570a2 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -485,24 +485,19 @@
     if (luceneModule != null) {
       return luceneModule;
     }
-    switch (indexType) {
-      case LUCENE:
-        return LuceneIndexModule.latestVersion(slave);
-      case ELASTICSEARCH:
-        return ElasticIndexModule.latestVersion(slave);
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (indexType.isLucene()) {
+      return LuceneIndexModule.latestVersion(slave);
     }
+    if (indexType.isElasticsearch()) {
+      return ElasticIndexModule.latestVersion(slave);
+    }
+    throw new IllegalStateException("unsupported index.type = " + indexType);
   }
 
   private void initIndexType() {
     indexType = IndexModule.getIndexType(cfgInjector);
-    switch (indexType) {
-      case LUCENE:
-      case ELASTICSEARCH:
-        break;
-      default:
-        throw new IllegalStateException("unsupported index.type = " + indexType);
+    if (!indexType.isLucene() && !indexType.isElasticsearch()) {
+      throw new IllegalStateException("unsupported index.type = " + indexType);
     }
   }
 
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 0e5f659..6d9fe59 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -147,16 +147,13 @@
     boolean slave = globalConfig.getBoolean("container", "slave", false);
     List<Module> modules = new ArrayList<>();
     Module indexModule;
-    switch (IndexModule.getIndexType(dbInjector)) {
-      case LUCENE:
-        indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
-        break;
-      case ELASTICSEARCH:
-        indexModule =
-            ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
-        break;
-      default:
-        throw new IllegalStateException("unsupported index.type");
+    IndexType indexType = IndexModule.getIndexType(dbInjector);
+    if (indexType.isLucene()) {
+      indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
+    } else if (indexType.isElasticsearch()) {
+      indexModule = ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
+    } else {
+      throw new IllegalStateException("unsupported index.type = " + indexType);
     }
     modules.add(indexModule);
     modules.add(new BatchProgramModule());
@@ -173,7 +170,7 @@
 
   private void overrideConfig() {
     // Disable auto-commit for speed; committing will happen at the end of the process.
-    if (IndexModule.getIndexType(dbInjector) == IndexType.LUCENE) {
+    if (IndexModule.getIndexType(dbInjector).isLucene()) {
       globalConfig.setLong("index", "changes_open", "commitWithin", -1);
       globalConfig.setLong("index", "changes_closed", "commitWithin", -1);
     }
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 9c158b7..df6a375 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.plugins.JarScanner;
 import com.google.gerrit.server.schema.NoteDbSchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
@@ -404,15 +405,13 @@
             }
           });
       Injector dbInjector = createDbInjector();
-      switch (IndexModule.getIndexType(dbInjector)) {
-        case LUCENE:
-          modules.add(new LuceneIndexModuleOnInit());
-          break;
-        case ELASTICSEARCH:
-          modules.add(new ElasticIndexModuleOnInit());
-          break;
-        default:
-          throw new IllegalStateException("unsupported index.type");
+      IndexType indexType = IndexModule.getIndexType(dbInjector);
+      if (indexType.isLucene()) {
+        modules.add(new LuceneIndexModuleOnInit());
+      } else if (indexType.isElasticsearch()) {
+        modules.add(new ElasticIndexModuleOnInit());
+      } else {
+        throw new IllegalStateException("unsupported index.type = " + indexType);
       }
       sysInjector = dbInjector.createChildInjector(modules);
     }
diff --git a/java/com/google/gerrit/pgm/init/InitIndex.java b/java/com/google/gerrit/pgm/init/InitIndex.java
index 0de08f2..5ede863 100644
--- a/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -53,27 +53,23 @@
 
   @Override
   public void run() throws IOException {
-    IndexType type = IndexType.LUCENE;
-    if (IndexType.values().length > 1) {
-      ui.header("Index");
-      type = index.select("Type", "type", type);
-    }
+    ui.header("Index");
+    IndexType type =
+        new IndexType(
+            index.select("Type", "type", IndexType.getDefault(), IndexType.getKnownTypes()));
 
-    if (type == IndexType.ELASTICSEARCH) {
+    if (type.isElasticsearch()) {
       Section elasticsearch = sections.get("elasticsearch", null);
       elasticsearch.string("Index Prefix", "prefix", "gerrit_");
       elasticsearch.string("Server", "server", "http://localhost:9200");
       index.string("Result window size", "maxLimit", "10000");
     }
 
-    if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
+    if ((site.isNew || isEmptySite()) && type.isLucene()) {
       for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
         IndexUtils.setReady(site, def.getName(), def.getLatest().getVersion(), true);
       }
     } else {
-      if (IndexType.values().length <= 1) {
-        ui.header("Index");
-      }
       String message =
           String.format(
               "\nThe index must be %sbuilt before starting Gerrit:\n"
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 9d09461..846bb82 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -115,6 +115,8 @@
     extractMailExample("CommentHtml.soy");
     extractMailExample("CommentFooter.soy");
     extractMailExample("CommentFooterHtml.soy");
+    extractMailExample("DeleteKey.soy");
+    extractMailExample("DeleteKeyHtml.soy");
     extractMailExample("DeleteReviewer.soy");
     extractMailExample("DeleteReviewerHtml.soy");
     extractMailExample("DeleteVote.soy");
@@ -122,6 +124,8 @@
     extractMailExample("Footer.soy");
     extractMailExample("FooterHtml.soy");
     extractMailExample("HeaderHtml.soy");
+    extractMailExample("HttpPasswordUpdate.soy");
+    extractMailExample("HttpPasswordUpdateHtml.soy");
     extractMailExample("InboundEmailRejection.soy");
     extractMailExample("InboundEmailRejectionHtml.soy");
     extractMailExample("Merged.soy");
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index baf37b6..cbf32a1 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -127,8 +127,10 @@
 
   public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
       String title, String name, T defValue, boolean nullIfDefault) {
+    @SuppressWarnings("rawtypes")
+    Class<? extends Enum> declaringClass = defValue.getDeclaringClass();
     @SuppressWarnings("unchecked")
-    E allowedValues = (E) EnumSet.allOf(defValue.getClass());
+    E allowedValues = (E) EnumSet.allOf(declaringClass);
     return select(title, name, defValue, allowedValues, nullIfDefault);
   }
 
diff --git a/java/com/google/gerrit/reviewdb/BUILD b/java/com/google/gerrit/reviewdb/BUILD
index d241140..8c286ce 100644
--- a/java/com/google/gerrit/reviewdb/BUILD
+++ b/java/com/google/gerrit/reviewdb/BUILD
@@ -13,6 +13,7 @@
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//proto:entities_java_proto",
     ],
diff --git a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
index 51e98c7..5c7b03b 100644
--- a/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/AccountIdProtoConverter.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum AccountIdProtoConverter implements ProtoConverter<Entities.Account_Id, Account.Id> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
index f1018fc..6fa9353 100644
--- a/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/BranchNameKeyProtoConverter.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum BranchNameKeyProtoConverter
     implements ProtoConverter<Entities.Branch_NameKey, BranchNameKey> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
index a89434e..5e90c87 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeIdProtoConverter.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeIdProtoConverter implements ProtoConverter<Entities.Change_Id, Change.Id> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
index b9a4f4d..4aa900b 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeKeyProtoConverter.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeKeyProtoConverter implements ProtoConverter<Entities.Change_Key, Change.Key> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
index 7d97e39..3d36293 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageKeyProtoConverter.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ChangeMessageKeyProtoConverter
     implements ProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
index 0895d8d..31b0e11 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeMessageProtoConverter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -22,6 +23,7 @@
 import java.sql.Timestamp;
 import java.util.Objects;
 
+@Immutable
 public enum ChangeMessageProtoConverter
     implements ProtoConverter<Entities.ChangeMessage, ChangeMessage> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
index 384dbca..2dfa516 100644
--- a/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ChangeProtoConverter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
@@ -22,6 +23,7 @@
 import com.google.protobuf.Parser;
 import java.sql.Timestamp;
 
+@Immutable
 public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Change> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
index 7bc2ab1..42049a4 100644
--- a/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/LabelIdProtoConverter.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum LabelIdProtoConverter implements ProtoConverter<Entities.LabelId, LabelId> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/ObjectIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ObjectIdProtoConverter.java
index 7413af9..246207d 100644
--- a/java/com/google/gerrit/reviewdb/converter/ObjectIdProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ObjectIdProtoConverter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
 import org.eclipse.jgit.lib.ObjectId;
@@ -30,6 +31,7 @@
  *   <li>This maintains backwards wire compatibility with a pre-NoteDb implementation.
  * </ul>
  */
+@Immutable
 public enum ObjectIdProtoConverter implements ProtoConverter<Entities.ObjectId, ObjectId> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
index 43f6295..d3136b1 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalKeyProtoConverter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.LabelId;
@@ -21,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum PatchSetApprovalKeyProtoConverter
     implements ProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
index 3baec99..a08d745 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetApprovalProtoConverter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -21,6 +22,7 @@
 import java.sql.Timestamp;
 import java.util.Objects;
 
+@Immutable
 public enum PatchSetApprovalProtoConverter
     implements ProtoConverter<Entities.PatchSetApproval, PatchSetApproval> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
index 4101a6b..154b0bf 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetIdProtoConverter.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum PatchSetIdProtoConverter implements ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
index f9ed8ef..5006906 100644
--- a/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/PatchSetProtoConverter.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.reviewdb.converter;
 
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -23,6 +24,7 @@
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
+@Immutable
 public enum PatchSetProtoConverter implements ProtoConverter<Entities.PatchSet, PatchSet> {
   INSTANCE;
 
diff --git a/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
index 74849af..99048a0 100644
--- a/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ProjectNameKeyProtoConverter.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.protobuf.Parser;
 
+@Immutable
 public enum ProjectNameKeyProtoConverter
     implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
   INSTANCE;
diff --git a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
index 568759c..f4f1de06 100644
--- a/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
+++ b/java/com/google/gerrit/reviewdb/converter/ProtoConverter.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.reviewdb.converter;
 
+import com.google.errorprone.annotations.Immutable;
 import com.google.protobuf.MessageLite;
 import com.google.protobuf.Parser;
 
+@Immutable
 public interface ProtoConverter<P extends MessageLite, C> {
 
   P toProto(C valueClass);
diff --git a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
index fc3e476..fff3274 100644
--- a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import java.util.Collection;
 import java.util.Optional;
@@ -82,6 +83,13 @@
   void clearReviewed(PatchSet.Id psId);
 
   /**
+   * Clears the reviewed flags for all files in all patch sets in the given change for all users.
+   *
+   * @param changeId change ID
+   */
+  void clearReviewed(Change.Id changeId);
+
+  /**
    * Find the latest patch set, that is smaller or equals to the given patch set, where at least,
    * one file has been reviewed by the given user.
    *
diff --git a/java/com/google/gerrit/server/change/ArchiveFormat.java b/java/com/google/gerrit/server/change/ArchiveFormat.java
index 0316c5f..d895a66 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -35,7 +35,9 @@
   TXZ("application/x-xz", new TxzFormat()),
   ZIP("application/x-zip", new ZipFormat());
 
+  @SuppressWarnings("ImmutableEnumChecker") // ArchiveCommand.Format is effectively immutable.
   private final ArchiveCommand.Format<?> format;
+
   private final String mimeType;
 
   ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index a56404d..2449df2 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -70,7 +70,7 @@
     ensureDeletable(ctx, id, patchSets);
     // Cleaning up is only possible as long as the change and its elements are
     // still part of the database.
-    cleanUpReferences(id, patchSets);
+    cleanUpReferences(id);
 
     ctx.deleteChange();
     changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
@@ -102,10 +102,8 @@
         revWalk.parseCommit(patchSet.commitId()), revWalk.parseCommit(destId.get()));
   }
 
-  private void cleanUpReferences(Change.Id id, Collection<PatchSet> patchSets) throws IOException {
-    for (PatchSet ps : patchSets) {
-      accountPatchReviewStore.run(s -> s.clearReviewed(ps.id()));
-    }
+  private void cleanUpReferences(Change.Id id) throws IOException {
+    accountPatchReviewStore.run(s -> s.clearReviewed(id));
 
     // Non-atomic operation on All-Users refs; not much we can do to make it atomic.
     starredChangesUtil.unstarAllForChangeDeletion(id);
diff --git a/java/com/google/gerrit/server/git/SystemReaderInstaller.java b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
index 520ede4..1043210 100644
--- a/java/com/google/gerrit/server/git/SystemReaderInstaller.java
+++ b/java/com/google/gerrit/server/git/SystemReaderInstaller.java
@@ -47,8 +47,6 @@
   private SystemReader customReader() {
     SystemReader current = SystemReader.getInstance();
 
-    FileBasedConfig jgitConfig = new FileBasedConfig(site.jgit_config.toFile(), FS.DETECTED);
-
     return new SystemReader() {
       @Override
       public String getHostname() {
@@ -67,12 +65,12 @@
 
       @Override
       public FileBasedConfig openUserConfig(Config parent, FS fs) {
-        return current.openSystemConfig(parent, fs);
+        return current.openUserConfig(parent, fs);
       }
 
       @Override
       public FileBasedConfig openSystemConfig(Config parent, FS fs) {
-        return jgitConfig;
+        return new FileBasedConfig(parent, site.jgit_config.toFile(), FS.DETECTED);
       }
 
       @Override
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index fe7ff86..d04c894 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -17,6 +17,8 @@
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
@@ -27,14 +29,12 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.List;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -56,6 +56,23 @@
         ProjectState projectState, BranchNameKey branch, IdentifiedUser user);
   }
 
+  /** A boolean validation status and a list of additional messages. */
+  @AutoValue
+  abstract static class Result {
+    static Result create(boolean isValid, ImmutableList<CommitValidationMessage> messages) {
+      return new AutoValue_BranchCommitValidator_Result(isValid, messages);
+    }
+
+    /** Whether the commit is valid. */
+    abstract boolean isValid();
+
+    /**
+     * A list of messages related to the validation. Messages may be present regardless of the
+     * {@link #isValid()} status.
+     */
+    abstract ImmutableList<CommitValidationMessage> messages();
+  }
+
   @Inject
   BranchCommitValidator(
       CommitValidators.Factory commitValidatorsFactory,
@@ -80,16 +97,17 @@
    * @param commit the commit being validated.
    * @param isMerged whether this is a merge commit created by magicBranch --merge option
    * @param change the change for which this is a new patchset.
+   * @return The validation {@link Result}.
    */
-  public boolean validCommit(
+  Result validateCommit(
       ObjectReader objectReader,
       ReceiveCommand cmd,
       RevCommit commit,
       boolean isMerged,
-      List<ValidationMessage> messages,
       NoteMap rejectCommits,
       @Nullable Change change)
       throws IOException {
+    ImmutableList.Builder<CommitValidationMessage> messages = new ImmutableList.Builder<>();
     try (CommitReceivedEvent receiveEvent =
         new CommitReceivedEvent(cmd, project, branch.branch(), objectReader, commit, user)) {
       CommitValidators validators;
@@ -123,9 +141,9 @@
                 messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
       }
       cmd.setResult(REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage(), objectReader));
-      return false;
+      return Result.create(false, messages.build());
     }
-    return true;
+    return Result.create(true, messages.build());
   }
 
   private String messageForCommit(RevCommit c, String msg, ObjectReader objectReader)
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 61249b1..73e0b1c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -62,6 +62,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.exceptions.StorageException;
@@ -1517,6 +1518,10 @@
       // TODO(dpursehouse): validate hashtags
     }
 
+    @UsedAt(UsedAt.Project.GOOGLE)
+    @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
+    private boolean createCodToken;
+
     MagicBranchInput(IdentifiedUser user, ReceiveCommand cmd, LabelTypes labelTypes) {
       this.deprecatedTopicSeen = false;
       this.cmd = cmd;
@@ -1949,14 +1954,16 @@
     BranchCommitValidator validator =
         commitValidatorFactory.create(projectState, changeEnt.getDest(), user);
     try {
-      if (validator.validCommit(
-          receivePack.getRevWalk().getObjectReader(),
-          cmd,
-          newCommit,
-          false,
-          messages,
-          rejectCommits,
-          changeEnt)) {
+      BranchCommitValidator.Result validationResult =
+          validator.validateCommit(
+              receivePack.getRevWalk().getObjectReader(),
+              cmd,
+              newCommit,
+              false,
+              rejectCommits,
+              changeEnt);
+      messages.addAll(validationResult.messages());
+      if (validationResult.isValid()) {
         logger.atFine().log("Replacing change %s", changeEnt.getId());
         requestReplace(cmd, true, changeEnt, newCommit);
       }
@@ -2114,14 +2121,16 @@
           logger.atFine().log("Creating new change for %s even though it is already tracked", name);
         }
 
-        if (!validator.validCommit(
-            receivePack.getRevWalk().getObjectReader(),
-            magicBranch.cmd,
-            c,
-            magicBranch.merged,
-            messages,
-            rejectCommits,
-            null)) {
+        BranchCommitValidator.Result validationResult =
+            validator.validateCommit(
+                receivePack.getRevWalk().getObjectReader(),
+                magicBranch.cmd,
+                c,
+                magicBranch.merged,
+                rejectCommits,
+                null);
+        messages.addAll(validationResult.messages());
+        if (!validationResult.isValid()) {
           // Not a change the user can propose? Abort as early as possible.
           logger.atFine().log("Aborting early due to invalid commit");
           return Collections.emptyList();
@@ -3113,8 +3122,10 @@
           continue;
         }
 
-        if (!validator.validCommit(
-            walk.getObjectReader(), cmd, c, false, messages, rejectCommits, null)) {
+        BranchCommitValidator.Result validationResult =
+            validator.validateCommit(walk.getObjectReader(), cmd, c, false, rejectCommits, null);
+        messages.addAll(validationResult.messages());
+        if (!validationResult.isValid()) {
           break;
         }
       }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 08c12ef..e839400 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -290,7 +290,7 @@
       }
       if (shouldPublishComments()) {
         boolean workInProgress = change.isWorkInProgress();
-        if (magicBranch != null && magicBranch.workInProgress) {
+        if (magicBranch.workInProgress) {
           workInProgress = true;
         }
         comments = publishComments(ctx, workInProgress);
diff --git a/java/com/google/gerrit/server/index/AbstractIndexModule.java b/java/com/google/gerrit/server/index/AbstractIndexModule.java
index 352ea4b..995b4b6 100644
--- a/java/com/google/gerrit/server/index/AbstractIndexModule.java
+++ b/java/com/google/gerrit/server/index/AbstractIndexModule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.index;
 
 import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -44,9 +43,21 @@
   @Override
   protected void configure() {
     if (slave) {
-      bind(AccountIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
-      bind(ChangeIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
-      bind(ProjectIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
+      bind(AccountIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
+      bind(ChangeIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
+      bind(ProjectIndex.Factory.class)
+          .toInstance(
+              s -> {
+                throw new UnsupportedOperationException();
+              });
     } else {
       install(
           new FactoryModuleBuilder()
@@ -74,11 +85,6 @@
     }
   }
 
-  @SuppressWarnings("unused")
-  private static <T> T createDummyIndexFactory(Schema<?> schema) {
-    throw new UnsupportedOperationException();
-  }
-
   protected abstract Class<? extends AccountIndex> getAccountIndex();
 
   protected abstract Class<? extends ChangeIndex> getChangeIndex();
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index 7dcad1a..899e061 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,9 +17,11 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.common.Nullable;
@@ -71,9 +73,36 @@
  * (e.g. Lucene).
  */
 public class IndexModule extends LifecycleModule {
-  public enum IndexType {
-    LUCENE,
-    ELASTICSEARCH
+  public static class IndexType {
+    private static final String LUCENE = "lucene";
+    private static final String ELASTICSEARCH = "elasticsearch";
+
+    private final String type;
+
+    public IndexType(@Nullable String type) {
+      this.type = type == null ? getDefault() : type.toLowerCase();
+    }
+
+    public static String getDefault() {
+      return LUCENE;
+    }
+
+    public static ImmutableSet<String> getKnownTypes() {
+      return ImmutableSet.of(LUCENE, ELASTICSEARCH);
+    }
+
+    public boolean isLucene() {
+      return type.equals(LUCENE);
+    }
+
+    public boolean isElasticsearch() {
+      return type.equals(ELASTICSEARCH);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("type", type).toString();
+    }
   }
 
   public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
@@ -90,7 +119,8 @@
 
   /** Type of secondary index. */
   public static IndexType getIndexType(@Nullable Config cfg) {
-    return cfg != null ? cfg.getEnum("index", null, "type", IndexType.LUCENE) : IndexType.LUCENE;
+    String configValue = cfg != null ? cfg.getString("index", null, "type") : null;
+    return new IndexType(configValue);
   }
 
   private final int threads;
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 577c255..3015187 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.index.SiteIndexer;
@@ -108,7 +109,7 @@
     int projectsFailed = 0;
     for (Project.NameKey name : projectCache.all()) {
       try (Repository repo = repoManager.openRepository(name)) {
-        long size = estimateSize(repo);
+        int size = estimateSize(repo);
         changeCount += size;
         projects.add(new ProjectHolder(name, size));
       } catch (IOException e) {
@@ -126,15 +127,17 @@
     return indexAll(index, projects);
   }
 
-  private long estimateSize(Repository repo) throws IOException {
+  private int estimateSize(Repository repo) throws IOException {
     // Estimate size based on IDs that show up in ref names. This is not perfect, since patch set
     // refs may exist for changes whose metadata was never successfully stored. But that's ok, as
     // the estimate is just used as a heuristic for sorting projects.
-    return repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES).stream()
-        .map(r -> Change.Id.fromRef(r.getName()))
-        .filter(Objects::nonNull)
-        .distinct()
-        .count();
+    long size =
+        repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES).stream()
+            .map(r -> Change.Id.fromRef(r.getName()))
+            .filter(Objects::nonNull)
+            .distinct()
+            .count();
+    return Ints.saturatedCast(size);
   }
 
   private SiteIndexer.Result indexAll(ChangeIndex index, SortedSet<ProjectHolder> projects) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 976813f..e92a0f6 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -155,7 +155,7 @@
 
     MutableInteger leafTerms = new MutableInteger();
     Predicate<ChangeData> out = rewriteImpl(in, index, opts, leafTerms);
-    if (in == out || out instanceof IndexPredicate) {
+    if (isSameInstance(in, out) || out instanceof IndexPredicate) {
       return new IndexedChangeQuery(index, out, opts);
     } else if (out == null /* cannot rewrite */) {
       return in;
@@ -207,7 +207,7 @@
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
       Predicate<ChangeData> nc = rewriteImpl(c, index, opts, leafTerms);
-      if (nc == c) {
+      if (isSameInstance(nc, c)) {
         isIndexed.set(i);
         newChildren.add(c);
       } else if (nc == null /* cannot rewrite c */) {
@@ -291,4 +291,9 @@
     return p.getChildCount() > 0
         && (p instanceof AndPredicate || p instanceof OrPredicate || p instanceof NotPredicate);
   }
+
+  @SuppressWarnings("ReferenceEquality")
+  private static <T> boolean isSameInstance(T a, T b) {
+    return a == b;
+  }
 }
diff --git a/java/com/google/gerrit/server/mail/EmailModule.java b/java/com/google/gerrit/server/mail/EmailModule.java
index 862293e..cc3db75 100644
--- a/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/java/com/google/gerrit/server/mail/EmailModule.java
@@ -20,8 +20,10 @@
 import com.google.gerrit.server.mail.send.AddReviewerSender;
 import com.google.gerrit.server.mail.send.CommentSender;
 import com.google.gerrit.server.mail.send.CreateChangeSender;
+import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.mail.send.DeleteVoteSender;
+import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.mail.send.MergedSender;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -37,8 +39,10 @@
     factory(AddReviewerSender.Factory.class);
     factory(CommentSender.Factory.class);
     factory(CreateChangeSender.Factory.class);
+    factory(DeleteKeySender.Factory.class);
     factory(DeleteReviewerSender.Factory.class);
     factory(DeleteVoteSender.Factory.class);
+    factory(HttpPasswordUpdateSender.Factory.class);
     factory(MergedSender.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
new file mode 100644
index 0000000..b95b596
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.util.Collections;
+import java.util.List;
+
+public class DeleteKeySender extends OutgoingEmail {
+  public interface Factory {
+    DeleteKeySender create(IdentifiedUser user, AccountSshKey sshKey);
+
+    DeleteKeySender create(IdentifiedUser user, List<String> gpgKeyFingerprints);
+  }
+
+  private final PermissionBackend permissionBackend;
+  private final IdentifiedUser callingUser;
+  private final IdentifiedUser user;
+  private final AccountSshKey sshKey;
+  private final List<String> gpgKeyFingerprints;
+
+  @AssistedInject
+  public DeleteKeySender(
+      EmailArguments ea,
+      PermissionBackend permissionBackend,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted AccountSshKey sshKey) {
+    super(ea, "deletekey");
+    this.permissionBackend = permissionBackend;
+    this.callingUser = callingUser;
+    this.user = user;
+    this.gpgKeyFingerprints = Collections.emptyList();
+    this.sshKey = sshKey;
+  }
+
+  @AssistedInject
+  public DeleteKeySender(
+      EmailArguments ea,
+      PermissionBackend permissionBackend,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted List<String> gpgKeyFingerprints) {
+    super(ea, "deletekey");
+    this.permissionBackend = permissionBackend;
+    this.callingUser = callingUser;
+    this.user = user;
+    this.gpgKeyFingerprints = gpgKeyFingerprints;
+    this.sshKey = null;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setHeader("Subject", String.format("[Gerrit Code Review] %s Keys Deleted", getKeyType()));
+    add(RecipientType.TO, new Address(getEmail()));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    if (user.equals(callingUser)) {
+      // Send email if the user self-removed a key; this notification is necessary to alert
+      // the user if their account was compromised and a key was unexpectedly deleted.
+      return true;
+    }
+
+    try {
+      // Don't email if an administrator removed a key on behalf of the user.
+      permissionBackend.user(callingUser).check(GlobalPermission.ADMINISTRATE_SERVER);
+      return false;
+    } catch (AuthException | PermissionBackendException e) {
+      // Send email if a non-administrator modified the keys, e.g. by MODIFY_ACCOUNT.
+      return true;
+    }
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(textTemplate("DeleteKey"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("DeleteKeyHtml"));
+    }
+  }
+
+  public String getEmail() {
+    return user.getAccount().getPreferredEmail();
+  }
+
+  public String getUserNameEmail() {
+    return getUserNameEmailFor(user.getAccountId());
+  }
+
+  public String getKeyType() {
+    if (sshKey != null) {
+      return "SSH";
+    } else if (gpgKeyFingerprints != null) {
+      return "GPG";
+    }
+    throw new IllegalStateException("key type is not SSH or GPG");
+  }
+
+  public String getSshKey() {
+    return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
+  }
+
+  public String getGpgKeyFingerprints() {
+    if (!gpgKeyFingerprints.isEmpty()) {
+      return Joiner.on("\n").join(gpgKeyFingerprints);
+    }
+    return null;
+  }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("email", getEmail());
+    soyContextEmailData.put("gpgKeyFingerprints", getGpgKeyFingerprints());
+    soyContextEmailData.put("keyType", getKeyType());
+    soyContextEmailData.put("sshKey", getSshKey());
+    soyContextEmailData.put("userNameEmail", getUserNameEmail());
+  }
+
+  @Override
+  protected boolean supportsHtml() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
new file mode 100644
index 0000000..ca332ff
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class HttpPasswordUpdateSender extends OutgoingEmail {
+  public interface Factory {
+    HttpPasswordUpdateSender create(IdentifiedUser user, String operation);
+  }
+
+  private final IdentifiedUser user;
+  private final String operation;
+
+  @AssistedInject
+  public HttpPasswordUpdateSender(
+      EmailArguments ea, @Assisted IdentifiedUser user, @Assisted String operation) {
+    super(ea, "HttpPasswordUpdate");
+    this.user = user;
+    this.operation = operation;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
+    add(RecipientType.TO, new Address(getEmail()));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    // Always send an email if the HTTP password is updated.
+    return true;
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(textTemplate("HttpPasswordUpdate"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("HttpPasswordUpdateHtml"));
+    }
+  }
+
+  public String getEmail() {
+    return user.getAccount().getPreferredEmail();
+  }
+
+  public String getUserNameEmail() {
+    return getUserNameEmailFor(user.getAccountId());
+  }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("email", getEmail());
+    soyContextEmailData.put("userNameEmail", getUserNameEmail());
+    soyContextEmailData.put("operation", operation);
+  }
+
+  @Override
+  protected boolean supportsHtml() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
index 8d7df41..3bb44c7 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -47,6 +47,8 @@
     "CommentHtml.soy",
     "CommentFooter.soy",
     "CommentFooterHtml.soy",
+    "DeleteKey.soy",
+    "DeleteKeyHtml.soy",
     "DeleteReviewer.soy",
     "DeleteReviewerHtml.soy",
     "DeleteVote.soy",
@@ -56,6 +58,8 @@
     "Footer.soy",
     "FooterHtml.soy",
     "HeaderHtml.soy",
+    "HttpPasswordUpdate.soy",
+    "HttpPasswordUpdateHtml.soy",
     "Merged.soy",
     "MergedHtml.soy",
     "NewChange.soy",
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 10d6ba5..f738367 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -131,6 +131,9 @@
     if (lastIndexSlash == 0) {
       return projectName.substring(1); // Remove the first slash
     }
+    if (lastIndexSlash == -1) { // No slash in the project name
+      return projectName;
+    }
 
     return "..." + projectName.substring(lastIndexSlash + 1);
   }
diff --git a/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
index fad9832..d5a7259 100644
--- a/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
+++ b/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -21,13 +21,13 @@
 /** State of a reviewer on a change. */
 public enum ReviewerStateInternal {
   /** The user has contributed at least one nonzero vote on the change. */
-  REVIEWER(new FooterKey("Reviewer"), ReviewerState.REVIEWER),
+  REVIEWER("Reviewer", ReviewerState.REVIEWER),
 
   /** The reviewer was added to the change, but has not voted. */
-  CC(new FooterKey("CC"), ReviewerState.CC),
+  CC("CC", ReviewerState.CC),
 
   /** The user was previously a reviewer on the change, but was removed. */
-  REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
+  REMOVED("Removed", ReviewerState.REMOVED);
 
   public static ReviewerStateInternal fromReviewerState(ReviewerState state) {
     return ReviewerStateInternal.values()[state.ordinal()];
@@ -50,20 +50,20 @@
     }
   }
 
-  private final FooterKey footerKey;
+  private final String footer;
   private final ReviewerState state;
 
-  ReviewerStateInternal(FooterKey footerKey, ReviewerState state) {
-    this.footerKey = footerKey;
+  ReviewerStateInternal(String footer, ReviewerState state) {
+    this.footer = footer;
     this.state = state;
   }
 
   FooterKey getFooterKey() {
-    return footerKey;
+    return new FooterKey(footer);
   }
 
   FooterKey getByEmailFooterKey() {
-    return new FooterKey(footerKey.getName() + "-email");
+    return new FooterKey(footer + "-email");
   }
 
   public ReviewerState asReviewerState() {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 61f0180..acf88e1 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -37,6 +37,7 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -500,8 +501,7 @@
       try {
         final boolean reuse;
         if (Patch.COMMIT_MSG.equals(path)) {
-          if (comparisonType.isAgainstParentOrAutoMerge()
-              && (aId == within || within.equals(aId))) {
+          if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
             id = ObjectId.zeroId();
             src = Text.EMPTY;
             srcContent = Text.NO_BYTES;
@@ -520,8 +520,7 @@
           }
           reuse = false;
         } else if (Patch.MERGE_LIST.equals(path)) {
-          if (comparisonType.isAgainstParentOrAutoMerge()
-              && (aId == within || within.equals(aId))) {
+          if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
             id = ObjectId.zeroId();
             src = Text.EMPTY;
             srcContent = Text.NO_BYTES;
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index e29a48a..d01954c 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -26,6 +26,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
@@ -329,6 +330,10 @@
     return as;
   }
 
+  public ImmutableSet<String> getAccessSectionNames() {
+    return ImmutableSet.copyOf(accessSections.keySet());
+  }
+
   public Collection<AccessSection> getAccessSections() {
     return sort(accessSections.values());
   }
diff --git a/java/com/google/gerrit/server/project/testing/TestLabels.java b/java/com/google/gerrit/server/project/testing/TestLabels.java
new file mode 100644
index 0000000..6c2ddde
--- /dev/null
+++ b/java/com/google/gerrit/server/project/testing/TestLabels.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project.testing;
+
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import java.util.Arrays;
+
+public class TestLabels {
+  public static LabelType codeReview() {
+    return label(
+        "Code-Review",
+        value(2, "Looks good to me, approved"),
+        value(1, "Looks good to me, but someone else must approve"),
+        value(0, "No score"),
+        value(-1, "I would prefer this is not merged as is"),
+        value(-2, "This shall not be merged"));
+  }
+
+  public static LabelType verified() {
+    return label("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
+  }
+
+  public static LabelType patchSetLock() {
+    LabelType label =
+        label("Patch-Set-Lock", value(1, "Patch Set Locked"), value(0, "Patch Set Unlocked"));
+    label.setFunction(LabelFunction.PATCH_SET_LOCK);
+    return label;
+  }
+
+  public static LabelValue value(int value, String text) {
+    return new LabelValue((short) value, text);
+  }
+
+  public static LabelType label(String name, LabelValue... values) {
+    return new LabelType(name, Arrays.asList(values));
+  }
+
+  private TestLabels() {}
+}
diff --git a/java/com/google/gerrit/server/project/testing/Util.java b/java/com/google/gerrit/server/project/testing/Util.java
deleted file mode 100644
index 2bd71c3..0000000
--- a/java/com/google/gerrit/server/project/testing/Util.java
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.project.testing;
-
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.LabelFunction;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.project.ProjectConfig;
-import java.util.Arrays;
-
-public class Util {
-  public static final AccountGroup.UUID ADMIN = AccountGroup.uuid("test.admin");
-  public static final AccountGroup.UUID DEVS = AccountGroup.uuid("test.devs");
-
-  public static final LabelType codeReview() {
-    return category(
-        "Code-Review",
-        value(2, "Looks good to me, approved"),
-        value(1, "Looks good to me, but someone else must approve"),
-        value(0, "No score"),
-        value(-1, "I would prefer this is not merged as is"),
-        value(-2, "This shall not be merged"));
-  }
-
-  public static final LabelType verified() {
-    return category("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
-  }
-
-  public static final LabelType patchSetLock() {
-    LabelType label =
-        category("Patch-Set-Lock", value(1, "Patch Set Locked"), value(0, "Patch Set Unlocked"));
-    label.setFunction(LabelFunction.PATCH_SET_LOCK);
-    return label;
-  }
-
-  public static LabelValue value(int value, String text) {
-    return new LabelValue((short) value, text);
-  }
-
-  public static LabelType category(String name, LabelValue... values) {
-    return new LabelType(name, Arrays.asList(values));
-  }
-
-  public static PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
-    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
-    group = project.resolve(group);
-
-    return new PermissionRule(group);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref);
-  }
-
-  public static PermissionRule allowExclusive(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref, true);
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project,
-      String permissionName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    PermissionRule r = grant(project, permissionName, rule, ref);
-    r.setBlock();
-    return r;
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    return grant(project, permissionName, newRule(project, group), ref);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String permissionName,
-      AccountGroup.UUID group,
-      String ref,
-      boolean exclusive) {
-    return grant(project, permissionName, newRule(project, group), ref, exclusive);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    return allow(project, capabilityName, group, (PermissionRange) null);
-  }
-
-  public static PermissionRule allow(
-      ProjectConfig project,
-      String capabilityName,
-      AccountGroup.UUID group,
-      PermissionRange customRange) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .add(rule);
-    if (GlobalCapability.hasRange(capabilityName)) {
-      if (customRange == null) {
-        PermissionRange.WithDefaults range = GlobalCapability.getRange(capabilityName);
-        if (range != null) {
-          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
-        }
-        return rule;
-      }
-      rule.setRange(customRange.getMin(), customRange.getMax());
-    }
-    return rule;
-  }
-
-  public static PermissionRule remove(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .remove(rule);
-    return rule;
-  }
-
-  public static PermissionRule remove(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule rule = newRule(project, group);
-    project.getAccessSection(ref, true).getPermission(permissionName, true).remove(rule);
-    return rule;
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project, String capabilityName, AccountGroup.UUID group) {
-    PermissionRule rule = newRule(project, group);
-    project
-        .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
-        .getPermission(capabilityName, true)
-        .add(rule);
-    return rule;
-  }
-
-  public static PermissionRule block(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
-    r.setBlock();
-    return r;
-  }
-
-  public static PermissionRule blockLabel(
-      ProjectConfig project, String labelName, AccountGroup.UUID group, String ref) {
-    return blockLabel(project, labelName, -1, 1, group, ref);
-  }
-
-  public static PermissionRule blockLabel(
-      ProjectConfig project,
-      String labelName,
-      int min,
-      int max,
-      AccountGroup.UUID group,
-      String ref) {
-    PermissionRule r = grant(project, Permission.LABEL + labelName, newRule(project, group), ref);
-    r.setBlock();
-    r.setRange(min, max);
-    return r;
-  }
-
-  public static PermissionRule deny(
-      ProjectConfig project, String permissionName, AccountGroup.UUID group, String ref) {
-    PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
-    r.setDeny();
-    return r;
-  }
-
-  public static void doNotInherit(ProjectConfig project, String permissionName, String ref) {
-    project
-        .getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .setExclusiveGroup(true);
-  }
-
-  private static PermissionRule grant(
-      ProjectConfig project, String permissionName, PermissionRule rule, String ref) {
-    return grant(project, permissionName, rule, ref, false);
-  }
-
-  private static PermissionRule grant(
-      ProjectConfig project,
-      String permissionName,
-      PermissionRule rule,
-      String ref,
-      boolean exclusive) {
-    Permission permission = project.getAccessSection(ref, true).getPermission(permissionName, true);
-    if (exclusive) {
-      permission.setExclusiveGroup(exclusive);
-    }
-    permission.add(rule);
-    return rule;
-  }
-
-  private Util() {}
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index aa167bb..3c43cb8 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -67,7 +67,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -742,7 +741,7 @@
   @Operator
   public Predicate<ChangeData> extension(String ext) throws QueryParseException {
     if (args.getSchema().hasField(ChangeField.EXTENSION)) {
-      if (ext.isEmpty() && IndexModule.getIndexType(cfg).equals(IndexType.ELASTICSEARCH)) {
+      if (ext.isEmpty() && IndexModule.getIndexType(cfg).isElasticsearch()) {
         return new FileWithNoExtensionInElasticPredicate();
       }
       return new FileExtensionPredicate(ext);
@@ -784,20 +783,15 @@
         return new RegexDirectoryPredicate(directory);
       }
 
-      DirectoryPredicate rootPredicate = new DirectoryPredicate(directory);
-      if (isRootAndRecursive(directory)) {
-        RegexDirectoryPredicate recursivePredicate = new RegexDirectoryPredicate("^.*");
-        return Predicate.or(rootPredicate, recursivePredicate);
+      if (IndexModule.getIndexType(cfg).isElasticsearch()
+          && (directory.isEmpty() || directory.equals("/"))) {
+        return Predicate.any();
       }
-      return rootPredicate;
+      return new DirectoryPredicate(directory);
     }
     throw new QueryParseException("'directory' operator is not supported by change index version");
   }
 
-  private static boolean isRootAndRecursive(String directory) {
-    return directory.isEmpty() || directory.equals("/");
-  }
-
   @Operator
   public Predicate<ChangeData> label(String name)
       throws QueryParseException, IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index ba06b89..66255f1 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.FieldBundle;
-import com.google.gerrit.index.query.ListResultSet;
+import com.google.gerrit.index.query.LazyResultSet;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.ResultSet;
@@ -25,6 +28,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
@@ -36,22 +40,29 @@
 
   @Override
   public ResultSet<ChangeData> read() {
-    // TODO(spearce) This probably should be more lazy.
-    //
-    List<ChangeData> r = new ArrayList<>();
-    Set<Change.Id> have = new HashSet<>();
-    for (Predicate<ChangeData> p : getChildren()) {
-      if (p instanceof ChangeDataSource) {
-        for (ChangeData cd : ((ChangeDataSource) p).read()) {
-          if (have.add(cd.getId())) {
-            r.add(cd);
-          }
-        }
-      } else {
-        throw new StorageException("No ChangeDataSource: " + p);
-      }
+    Optional<Predicate<ChangeData>> nonChangeDataSource =
+        getChildren().stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
+    if (nonChangeDataSource.isPresent()) {
+      throw new StorageException("No ChangeDataSource: " + nonChangeDataSource.get());
     }
-    return new ListResultSet<>(r);
+
+    // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
+    // requested allows the index to run asynchronous queries.
+    List<ResultSet<ChangeData>> results =
+        getChildren().stream().map(p -> ((ChangeDataSource) p).read()).collect(toImmutableList());
+    return new LazyResultSet<>(
+        () -> {
+          List<ChangeData> r = new ArrayList<>();
+          Set<Change.Id> have = new HashSet<>();
+          for (ResultSet<ChangeData> resultSet : results) {
+            for (ChangeData result : resultSet) {
+              if (have.add(result.getId())) {
+                r.add(result);
+              }
+            }
+          }
+          return ImmutableList.copyOf(r);
+        });
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index 787e083..054f4bc 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -14,13 +14,17 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -34,22 +38,26 @@
 
 @Singleton
 public class DeleteSshKey implements RestModifyView<AccountResource.SshKey, Input> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
+  private final DeleteKeySender.Factory deleteKeySenderFactory;
 
   @Inject
   DeleteSshKey(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
-      SshKeyCache sshKeyCache) {
+      SshKeyCache sshKeyCache,
+      DeleteKeySender.Factory deleteKeySenderFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.authorizedKeys = authorizedKeys;
     this.sshKeyCache = sshKeyCache;
+    this.deleteKeySenderFactory = deleteKeySenderFactory;
   }
 
   @Override
@@ -60,8 +68,15 @@
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
-    authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().seq());
-    rsrc.getUser().getUserName().ifPresent(sshKeyCache::evict);
+    IdentifiedUser user = rsrc.getUser();
+    authorizedKeys.deleteKey(user.getAccountId(), rsrc.getSshKey().seq());
+    try {
+      deleteKeySenderFactory.create(user, rsrc.getSshKey()).send();
+    } catch (EmailException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot send SSH key deletion message to %s", user.getAccount().getPreferredEmail());
+    }
+    user.getUserName().ifPresent(sshKeyCache::evict);
 
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 04593f6..41dcce0 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -17,7 +17,9 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.common.HttpPasswordInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -31,6 +33,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -44,6 +47,8 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class PutHttpPassword implements RestModifyView<AccountResource, HttpPasswordInput> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final int LEN = 31;
   private static final SecureRandom rng;
 
@@ -59,17 +64,20 @@
   private final PermissionBackend permissionBackend;
   private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
+  private final HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory;
 
   @Inject
   PutHttpPassword(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       ExternalIds externalIds,
-      @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+      @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+      HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
+    this.httpPasswordUpdateSenderFactory = httpPasswordUpdateSenderFactory;
   }
 
   @Override
@@ -112,6 +120,15 @@
                     ExternalId.createWithPassword(
                         extId.key(), extId.accountId(), extId.email(), newPassword)));
 
+    try {
+      httpPasswordUpdateSenderFactory
+          .create(user, newPassword == null ? "deleted" : "added or updated")
+          .send();
+    } catch (EmailException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot send HttpPassword update message to %s", user.getAccount().getPreferredEmail());
+    }
+
     return Strings.isNullOrEmpty(newPassword) ? Response.none() : Response.ok(newPassword);
   }
 
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 4877eed..d7dbf58 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -295,6 +296,18 @@
   }
 
   @Override
+  public void clearReviewed(Change.Id changeId) {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement("DELETE FROM account_patch_reviews WHERE change_id = ?")) {
+      stmt.setInt(1, changeId.get());
+      stmt.executeUpdate();
+    } catch (SQLException e) {
+      throw convertError("delete", e);
+    }
+  }
+
+  @Override
   public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
     try (Connection con = ds.getConnection();
         PreparedStatement stmt =
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index 7fc47dc..8ad063a 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -646,7 +646,7 @@
         int newSize = msgbuf.length() + bullet.length() + message.length();
         if (++numMessages > maxCommitMessages
             || newSize > maxCombinedCommitMessageSize
-            || iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize) {
+            || (iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize)) {
           msgbuf.append(ellipsis);
           break;
         }
diff --git a/java/com/google/gerrit/server/util/IdGenerator.java b/java/com/google/gerrit/server/util/IdGenerator.java
index 276df06..d4c2dc4 100644
--- a/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/java/com/google/gerrit/server/util/IdGenerator.java
@@ -45,8 +45,8 @@
   public static int mix(int salt, int in) {
     short v0 = hi16(in);
     short v1 = lo16(in);
-    v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
-    v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
+    v0 += (short) (((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1);
+    v1 += (short) (((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3);
     return result(v0, v1);
   }
 
@@ -54,8 +54,8 @@
   static int unmix(int in) {
     short v0 = hi16(in);
     short v1 = lo16(in);
-    v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3;
-    v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
+    v1 -= (short) (((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3);
+    v0 -= (short) (((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1);
     return result(v0, v1);
   }
 
diff --git a/java/com/google/gerrit/sshd/commands/StreamEvents.java b/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 447f7ec..c680d30 100644
--- a/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -87,29 +87,6 @@
     EventTypes.register(DroppedOutputEvent.TYPE, DroppedOutputEvent.class);
   }
 
-  private final CancelableRunnable writer =
-      new CancelableRunnable() {
-        @Override
-        public void run() {
-          writeEvents();
-        }
-
-        @Override
-        public void cancel() {
-          onExit(0);
-        }
-
-        @Override
-        public String toString() {
-          StringBuilder b = new StringBuilder();
-          b.append("Stream Events");
-          if (currentUser.getUserName().isPresent()) {
-            b.append(" (").append(currentUser.getUserName().get()).append(")");
-          }
-          return b.toString();
-        }
-      };
-
   /** True if {@link DroppedOutputEvent} needs to be sent. */
   private volatile boolean dropped;
 
@@ -127,8 +104,6 @@
    */
   private Future<?> task;
 
-  private PrintWriter stdout;
-
   @Override
   public void start(Environment env) throws IOException {
     try {
@@ -144,7 +119,30 @@
       return;
     }
 
-    stdout = toPrintWriter(out);
+    PrintWriter stdout = toPrintWriter(out);
+    CancelableRunnable writer =
+        new CancelableRunnable() {
+          @Override
+          public void run() {
+            writeEvents(this, stdout);
+          }
+
+          @Override
+          public void cancel() {
+            onExit(0);
+          }
+
+          @Override
+          public String toString() {
+            StringBuilder b = new StringBuilder();
+            b.append("Stream Events");
+            if (currentUser.getUserName().isPresent()) {
+              b.append(" (").append(currentUser.getUserName().get()).append(")");
+            }
+            return b.toString();
+          }
+        };
+
     eventListenerRegistration =
         eventListeners.add(
             "gerrit",
@@ -152,7 +150,7 @@
               @Override
               public void onEvent(Event event) {
                 if (subscribedToEvents.isEmpty() || subscribedToEvents.contains(event.getType())) {
-                  offer(event);
+                  offer(writer, event);
                 }
               }
 
@@ -199,7 +197,7 @@
     }
   }
 
-  private void offer(Event event) {
+  private void offer(CancelableRunnable writer, Event event) {
     synchronized (taskLock) {
       if (!queue.offer(event)) {
         dropped = true;
@@ -221,7 +219,7 @@
     }
   }
 
-  private void writeEvents() {
+  private void writeEvents(CancelableRunnable writer, PrintWriter stdout) {
     int processed = 0;
 
     while (processed < BATCH_SIZE) {
@@ -231,13 +229,13 @@
         // accepting output. Either way terminate this instance.
         //
         removeEventListenerRegistration();
-        flush();
+        flush(stdout);
         onExit(0);
         return;
       }
 
       if (dropped) {
-        write(new DroppedOutputEvent());
+        write(stdout, new DroppedOutputEvent());
         dropped = false;
       }
 
@@ -246,11 +244,11 @@
         break;
       }
 
-      write(event);
+      write(stdout, event);
       processed++;
     }
 
-    flush();
+    flush(stdout);
 
     if (BATCH_SIZE <= processed) {
       // We processed the limit, but more might remain in the queue.
@@ -263,7 +261,7 @@
     }
   }
 
-  private void write(Object message) {
+  private void write(PrintWriter stdout, Object message) {
     String msg = null;
     try {
       msg = gson.toJson(message) + "\n";
@@ -277,7 +275,7 @@
     }
   }
 
-  private void flush() {
+  private void flush(PrintWriter stdout) {
     synchronized (stdout) {
       stdout.flush();
     }
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 24f82a7..a58e472 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -139,7 +139,7 @@
     PacketLineIn packetIn = new PacketLineIn(in);
     for (; ; ) {
       String s = packetIn.readString();
-      if (s == PacketLineIn.END) {
+      if (isPacketLineEnd(s)) {
         break;
       }
       if (!s.startsWith(argCmd)) {
@@ -163,6 +163,12 @@
     }
   }
 
+  // JGit API depends on reference equality with sentinel.
+  @SuppressWarnings({"ReferenceEquality", "StringEquality"})
+  private static boolean isPacketLineEnd(String s) {
+    return s == PacketLineIn.END;
+  }
+
   @Override
   protected void runImpl() throws IOException, PermissionBackendException, Failure {
     PacketLineOut packetOut = new PacketLineOut(out);
diff --git a/java/com/google/gerrit/testing/AssertableExecutorService.java b/java/com/google/gerrit/testing/AssertableExecutorService.java
index fabd7b78..18ac2e9 100644
--- a/java/com/google/gerrit/testing/AssertableExecutorService.java
+++ b/java/com/google/gerrit/testing/AssertableExecutorService.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.testing;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.util.concurrent.ForwardingExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
@@ -57,8 +57,8 @@
 
   /** Asserts and resets the number of executions this executor observed. */
   public void assertInteractions(int expectedNumInteractions) {
-    assertThat(numInteractions.get())
-        .named("expectedRunnablesSubmittedOnExecutor")
+    assertWithMessage("expectedRunnablesSubmittedOnExecutor")
+        .that(numInteractions.get())
         .isEqualTo(expectedNumInteractions);
     numInteractions.set(0);
   }
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index f896eaa..27065aa 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -15,6 +15,7 @@
         "//lib/powermock:powermock-module-junit4-common",
     ],
     deps = [
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/exceptions",
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 98ac13b..5f1826b 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -20,6 +20,8 @@
 import com.google.common.base.Strings;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -223,28 +225,19 @@
     bind(AllChangesIndexer.class).toProvider(Providers.of(null));
     bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
 
-    IndexType indexType = null;
-    try {
-      indexType = cfg.getEnum("index", null, "type", IndexType.LUCENE);
-    } catch (IllegalArgumentException e) {
-      // Custom index type, caller must provide their own module.
-    }
-    if (indexType != null) {
-      switch (indexType) {
-        case LUCENE:
-          install(luceneIndexModule());
-          break;
-        case ELASTICSEARCH:
-          install(elasticIndexModule());
-          break;
-        default:
-          throw new ProvisionException("index type unsupported in tests: " + indexType);
-      }
+    IndexType indexType = new IndexType(cfg.getString("index", null, "type"));
+    // For custom index types, callers must provide their own module.
+    if (indexType.isLucene()) {
+      install(luceneIndexModule());
+    } else if (indexType.isElasticsearch()) {
+      install(elasticIndexModule());
     }
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
     install(new RestApiModule());
     install(new DefaultProjectNameLockManager.Module());
+
+    bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
   }
 
   /** Copy of SchemaModule with a slightly different server ID provider. */
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index fc04204..f653ef1 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -17,10 +17,15 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.gpg.testing.TestKeys.allValidKeys;
@@ -417,8 +422,10 @@
       List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
       assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
 
-      RevCommit commitUserBranch = getRemoteHead(allUsers, RefNames.refsUsers(accountId));
-      RevCommit commitRefsMetaExternalIds = getRemoteHead(allUsers, RefNames.REFS_EXTERNAL_IDS);
+      RevCommit commitUserBranch =
+          projectOperations.project(allUsers).getHead(RefNames.refsUsers(accountId));
+      RevCommit commitRefsMetaExternalIds =
+          projectOperations.project(allUsers).getHead(RefNames.REFS_EXTERNAL_IDS);
       assertThat(commitUserBranch.getCommitTime())
           .isEqualTo(commitRefsMetaExternalIds.getCommitTime());
     } finally {
@@ -527,18 +534,15 @@
     accountIndexedCounter.assertReindexOf(user);
 
     // Inactive users may only be resolved by ID.
-    try {
-      gApi.accounts().id("user");
-      assert_().fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Account 'user' only matches inactive accounts. To use an inactive account, retry"
-                  + " with one of the following exact account IDs:\n"
-                  + id
-                  + ": User <user@example.com>");
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id("user"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Account 'user' only matches inactive accounts. To use an inactive account, retry"
+                + " with one of the following exact account IDs:\n"
+                + id
+                + ": User <user@example.com>");
     assertThat(gApi.accounts().id(id).getActive()).isFalse();
 
     gApi.accounts().id(id).setActive(true);
@@ -575,12 +579,11 @@
     try {
       /* Test account that can be activated, but not deactivated */
       // Deactivate account that is already inactive
-      try {
-        gApi.accounts().id(activatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("account not active");
-      }
+      ResourceConflictException thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("account not active");
       assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
 
       // Activate account that can be activated
@@ -592,12 +595,11 @@
       assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
 
       // Try deactivating account that cannot be deactivated
-      try {
-        gApi.accounts().id(activatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("not allowed to deactive account");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("not allowed to deactive account");
       assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
 
       /* Test account that can be deactivated, but not activated */
@@ -610,21 +612,19 @@
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
 
       // Deactivate account that is already inactive
-      try {
-        gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("account not active");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(deactivatableAccountId.get()).setActive(false));
+      assertThat(thrown).hasMessageThat().isEqualTo("account not active");
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
 
       // Try activating account that cannot be activated
-      try {
-        gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
-        fail("Expected exception");
-      } catch (ResourceConflictException e) {
-        assertThat(e.getMessage()).isEqualTo("not allowed to active account");
-      }
+      thrown =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.accounts().id(deactivatableAccountId.get()).setActive(true));
+      assertThat(thrown).hasMessageThat().isEqualTo("not allowed to active account");
       assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
     } finally {
       registrationHandle.remove();
@@ -645,12 +645,10 @@
     assertThat(gApi.accounts().id("user").getActive()).isTrue();
     gApi.accounts().id("user").setActive(false);
     assertThat(gApi.accounts().id(id).getActive()).isFalse();
-    try {
-      gApi.accounts().id(id).setActive(false);
-      fail("Expected exception");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage()).isEqualTo("account not active");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.accounts().id(id).setActive(false));
+    assertThat(thrown).hasMessageThat().isEqualTo("account not active");
     gApi.accounts().id(id).setActive(true);
   }
 
@@ -981,12 +979,9 @@
             "new.email@example.africa");
     for (String email : emails) {
       EmailInput input = newEmailInput(email);
-      try {
-        gApi.accounts().self().addEmail(input);
-        fail("Expected BadRequestException for invalid email address: " + email);
-      } catch (BadRequestException e) {
-        assertThat(e).hasMessageThat().isEqualTo("invalid email address");
-      }
+      BadRequestException thrown =
+          assertThrows(BadRequestException.class, () -> gApi.accounts().self().addEmail(input));
+      assertWithMessage(email).that(thrown).hasMessageThat().isEqualTo("invalid email address");
     }
     accountIndexedCounter.assertNoReindex();
   }
@@ -1244,7 +1239,10 @@
   @Test
   @Sandboxed
   public void userCanSetNameOfOtherUserWithModifyAccountPermission() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.MODIFY_ACCOUNT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+        .update();
     gApi.accounts().id(admin.username()).setName("Admin McAdminface");
     assertThat(gApi.accounts().id(admin.username()).get().name).isEqualTo("Admin McAdminface");
   }
@@ -1265,23 +1263,24 @@
     }
 
     // deny READ permission that is inherited from All-Projects
-    deny(allUsers, RefNames.REFS + "*", Permission.READ, ANONYMOUS_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(deny(Permission.READ).ref(RefNames.REFS + "*").group(ANONYMOUS_USERS))
+        .update();
 
     // fetching user branch without READ permission fails
-    try {
-      fetch(allUsersRepo, userRefName + ":userRef");
-      fail("user branch is visible although no READ permission is granted");
-    } catch (TransportException e) {
-      // expected because no READ granted on user branch
-    }
+    assertThrows(TransportException.class, () -> fetch(allUsersRepo, userRefName + ":userRef"));
 
     // allow each user to read its own user branch
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.READ,
-        false,
-        REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(
+            allow(Permission.READ)
+                .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                .group(REGISTERED_USERS))
+        .update();
 
     // fetch user branch using refs/users/YY/XXXXXXX
     fetch(allUsersRepo, userRefName + ":userRef");
@@ -1531,16 +1530,23 @@
 
   @Test
   public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestAccount foo = accountCreator.create(name("foo"));
     assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
     String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
-    grantLabel("Code-Review", -2, 2, allUsers, userRef, adminGroupUuid(), false);
-    grant(allUsers, userRef, Permission.SUBMIT, false, adminGroupUuid());
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+        .add(allowLabel("Code-Review").ref(userRef).group(adminGroupUuid()).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref(userRef).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
@@ -1705,7 +1711,11 @@
         .update("Set Preferred Email", foo.id(), u -> u.setPreferredEmail(noEmail));
     accountIndexedCounter.clear();
 
-    grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(userRef).group(REGISTERED_USERS))
+        .update();
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
     fetch(allUsersRepo, userRef + ":userRef");
     allUsersRepo.reset("userRef");
@@ -1737,7 +1747,11 @@
     String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
     fetch(allUsersRepo, userRef + ":userRef");
@@ -1788,14 +1802,21 @@
 
   @Test
   public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestAccount foo = accountCreator.create(name("foo"));
     assertThat(gApi.accounts().id(foo.id().get()).getActive()).isTrue();
     String userRef = RefNames.refsUsers(foo.id());
     accountIndexedCounter.clear();
 
-    grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(userRef).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRef + ":userRef");
@@ -1820,8 +1841,12 @@
 
   @Test
   public void cannotCreateUserBranch() throws Exception {
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
 
     String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -1836,9 +1861,16 @@
 
   @Test
   public void createUserBranchWithAccessDatabaseCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
 
     String userRef = RefNames.refsUsers(Account.id(seq.nextAccountId()));
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -1852,9 +1884,16 @@
   @Test
   public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
       throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(adminGroupUuid()))
+        .update();
 
     String userRef = RefNames.REFS_USERS + "foo";
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -1873,8 +1912,12 @@
       assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull();
     }
 
-    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.CREATE);
-    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS_DEFAULT).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS_DEFAULT).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     pushFactory
@@ -1889,12 +1932,15 @@
 
   @Test
   public void cannotDeleteUserBranch() throws Exception {
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.DELETE,
-        true,
-        REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(
+            allow(Permission.DELETE)
+                .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                .group(REGISTERED_USERS)
+                .force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     String userRef = RefNames.refsUsers(admin.id());
@@ -1910,13 +1956,19 @@
 
   @Test
   public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    grant(
-        allUsers,
-        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
-        Permission.DELETE,
-        true,
-        REGISTERED_USERS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(
+            allow(Permission.DELETE)
+                .ref(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}")
+                .group(REGISTERED_USERS)
+                .force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     String userRef = RefNames.refsUsers(admin.id());
@@ -2017,9 +2069,12 @@
     addGpgKey(key.getPublicKeyArmored());
     assertKeys(key);
 
+    sender.clear();
     gApi.accounts().self().gpgKey(id).delete();
     accountIndexedCounter.assertReindexOf(admin);
     assertKeys();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("GPG keys have been deleted");
 
     ResourceNotFoundException thrown =
         assertThrows(
@@ -2126,6 +2181,7 @@
     assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
 
     // Delete second key
+    sender.clear();
     gApi.accounts().self().deleteSshKey(2);
     info = gApi.accounts().self().listSshKeys();
     assertThat(info).hasSize(2);
@@ -2133,6 +2189,9 @@
     assertThat(info.get(1).seq).isEqualTo(3);
     accountIndexedCounter.assertReindexOf(admin);
 
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("SSH keys have been deleted");
+
     // Mark first key as invalid
     assertThat(info.get(0).valid).isTrue();
     authorizedKeys.markKeyInvalid(admin.id(), 1);
@@ -2165,7 +2224,10 @@
 
   @Test
   public void checkConsistency() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     // Create an account with a preferred email.
@@ -2289,12 +2351,9 @@
             "@", "@foo", "-", "-foo", "_", "_foo", "!", "+", "{", "}", "*", "%", "#", "$", "&", "’",
             "^", "=", "~");
     for (String name : invalidNames) {
-      try {
-        gApi.accounts().create(name);
-        fail(String.format("Expected BadRequestException for username [%s]", name));
-      } catch (BadRequestException e) {
-        assertThat(e).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
-      }
+      BadRequestException thrown =
+          assertThrows(BadRequestException.class, () -> gApi.accounts().create(name));
+      assertThat(thrown).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
     }
   }
 
@@ -2430,12 +2489,9 @@
     assertThat(accountInfo.status).isNull();
     assertThat(accountInfo.name).isNotEqualTo(fullName);
 
-    try {
-      update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName));
-      fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Ignore, expected
-    }
+    assertThrows(
+        LockFailureException.class,
+        () -> update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName)));
     assertThat(bgCounter.get()).isEqualTo(status.size());
 
     Account updatedAccount = accounts.get(admin.id()).get().getAccount();
@@ -2508,7 +2564,10 @@
 
   @Test
   public void atomicReadMofifyWriteExternalIds() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId extIdA1 = ExternalId.create("foo", "A-1", accountId);
@@ -2753,12 +2812,14 @@
       requestScopeOperations.setApiUser(user.id());
       createDraft(r, PushOneCommit.FILE_NAME, "draft");
       requestScopeOperations.setApiUser(admin.id());
-      try {
-        gApi.accounts().id(user.id().get()).deleteDraftComments(new DeleteDraftCommentsInput());
-        assert_().fail("expected AuthException");
-      } catch (AuthException e) {
-        assertThat(e).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
-      }
+      AuthException thrown =
+          assertThrows(
+              AuthException.class,
+              () ->
+                  gApi.accounts()
+                      .id(user.id().get())
+                      .deleteDraftComments(new DeleteDraftCommentsInput()));
+      assertThat(thrown).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
     } finally {
       cleanUpDrafts();
     }
@@ -2777,14 +2838,22 @@
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
 
-      block(project, "refs/heads/secret", Permission.READ, REGISTERED_USERS);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(block(Permission.READ).ref("refs/heads/secret").group(REGISTERED_USERS))
+          .update();
       List<DeletedDraftCommentInfo> result =
           gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
       assertThat(result).hasSize(1);
       assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
       assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
 
-      removePermission(project, "refs/heads/secret", Permission.READ);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.READ).ref("refs/heads/secret"))
+          .update();
       assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
       // Draft still exists since change wasn't visible when drafts where deleted.
       assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
@@ -2795,15 +2864,21 @@
 
   @Test
   public void userCanGenerateNewHttpPassword() throws Exception {
+    sender.clear();
     String newPassword = gApi.accounts().self().generateHttpPassword();
     assertThat(newPassword).isNotNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
   public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
     requestScopeOperations.setApiUser(admin.id());
+    sender.clear();
     String newPassword = gApi.accounts().id(user.username()).generateHttpPassword();
     assertThat(newPassword).isNotNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
@@ -2831,7 +2906,10 @@
   @Test
   public void userCanRemoveHttpPassword() throws Exception {
     requestScopeOperations.setApiUser(user.id());
+    sender.clear();
     assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
   }
 
   @Test
@@ -2845,14 +2923,20 @@
   public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
     requestScopeOperations.setApiUser(admin.id());
     String httpPassword = "new-password-for-user";
+    sender.clear();
     assertThat(gApi.accounts().id(user.username()).setHttpPassword(httpPassword))
         .isEqualTo(httpPassword);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
   }
 
   @Test
   public void adminCanRemoveHttpPasswordForUser() throws Exception {
     requestScopeOperations.setApiUser(admin.id());
+    sender.clear();
     assertThat(gApi.accounts().id(user.username()).setHttpPassword(null)).isNull();
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 07bd7ee..8730486 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -335,12 +335,9 @@
     AuthRequest who = AuthRequest.forUser(username);
     who.setActive(false);
     who.setAuthProvidesAccountActiveStatus(true);
-    try {
-      accountManager.authenticate(who);
-      fail("Expected AccountException");
-    } catch (AccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Authentication error, account inactive");
-    }
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown).hasMessageThat().isEqualTo("Authentication error, account inactive");
 
     Optional<AccountState> accountState = accounts.get(accountId);
     assertThat(accountState).isPresent();
@@ -422,12 +419,11 @@
     // Expect that this fails because the new email is already assigned to the other account.
     AuthRequest who = AuthRequest.forUser(username);
     who.setEmailAddress(newEmail);
-    try {
-      accountManager.authenticate(who);
-      fail("Expected AccountException");
-    } catch (AccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Email 'bar@example.com' in use by another account");
-    }
+    AccountException thrown =
+        assertThrows(AccountException.class, () -> accountManager.authenticate(who));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Email 'bar@example.com' in use by another account");
 
     // Verify that the email in the external ID was not updated.
     Optional<ExternalId> gerritExtId = externalIds.get(gerritExtIdKey);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 382b24b..6dc8fc5 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -315,12 +315,9 @@
 
     // Create a change is not allowed when CLA is required but not signed
     setUseContributorAgreements(InheritableBoolean.TRUE);
-    try {
-      gApi.changes().create(newChangeInput());
-      fail("Expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e.getMessage()).contains("Contributor Agreement");
-    }
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> gApi.changes().create(newChangeInput()));
+    assertThat(thrown).hasMessageThat().contains("Contributor Agreement");
 
     // Sign the agreement
     gApi.accounts().self().signAgreement(caAutoVerify.getName());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 6a116d8..87f9a2d5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -27,6 +28,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -47,8 +49,9 @@
 
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
-  @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ChangeCleanupConfig cleanupConfig;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   public void abandon() throws Exception {
@@ -174,7 +177,11 @@
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
-    grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).abandon();
     assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index d9699fd..7948ca5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -22,6 +22,13 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
@@ -45,8 +52,8 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -98,7 +105,6 @@
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
-import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -150,7 +156,7 @@
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
@@ -300,7 +306,11 @@
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setWorkInProgress();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
@@ -348,7 +358,11 @@
     gApi.changes().id(changeId).setWorkInProgress();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user2.id());
     gApi.changes().id(changeId).setReadyForReview();
     assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
@@ -507,12 +521,14 @@
     String refactor = "Needs some refactoring";
     String ptal = "PTAL";
 
-    grant(
-        project,
-        "refs/heads/master",
-        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
-        false,
-        REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+                .ref("refs/heads/master")
+                .group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(changeId).setWorkInProgress(refactor);
@@ -632,12 +648,14 @@
   public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    grant(
-        project,
-        "refs/heads/master",
-        Permission.TOGGLE_WORK_IN_PROGRESS_STATE,
-        false,
-        REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+                .ref("refs/heads/master")
+                .group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).current().review(in);
     ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
@@ -953,7 +971,11 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
@@ -973,8 +995,12 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS);
-    block("refs/for/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
@@ -996,7 +1022,11 @@
     revision.review(ReviewInput.approve());
     revision.submit();
 
-    block("refs/for/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Rebase the second
     String changeId = r2.getChangeId();
@@ -1025,7 +1055,11 @@
 
   @Test
   public void deleteNewChangeAsUserWithDeleteChangesPermissionForGroup() throws Exception {
-    allow("refs/*", Permission.DELETE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     deleteChangeAsUser(admin, user);
   }
 
@@ -1034,32 +1068,40 @@
     GroupApi groupApi = gApi.groups().create(name("delete-change"));
     groupApi.addMembers("user");
 
+    Project.NameKey nameKey = Project.nameKey(name("delete-change"));
     ProjectInput in = new ProjectInput();
-    in.name = name("delete-change");
+    in.name = nameKey.get();
     in.owners = Lists.newArrayListWithCapacity(1);
     in.owners.add(groupApi.name());
     in.createEmptyCommit = true;
-    ProjectApi api = gApi.projects().create(in);
+    gApi.projects().create(in);
 
-    Project.NameKey nameKey = Project.nameKey(api.get().name);
-
-    try (ProjectConfigUpdate u = updateProject(nameKey)) {
-      Util.allow(u.getConfig(), Permission.DELETE_CHANGES, PROJECT_OWNERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(nameKey)
+        .forUpdate()
+        .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(PROJECT_OWNERS))
+        .update();
 
     deleteChangeAsUser(nameKey, admin, user);
   }
 
   @Test
   public void deleteChangeAsUserWithDeleteOwnChangesPermissionForGroup() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     deleteChangeAsUser(user, user);
   }
 
   @Test
   public void deleteChangeAsUserWithDeleteOwnChangesPermissionForOwners() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, CHANGE_OWNER);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(CHANGE_OWNER))
+        .update();
     deleteChangeAsUser(user, user);
   }
 
@@ -1097,8 +1139,12 @@
       eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
       eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
-      removePermission(project, "refs/*", Permission.DELETE_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .remove(permissionKey(Permission.DELETE_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1109,7 +1155,11 @@
 
   @Test
   public void deleteNewChangeOfAnotherUserWithDeleteOwnChangesPermission() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     try {
       PushOneCommit.Result changeResult = createChange();
@@ -1120,7 +1170,11 @@
           assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
       assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1179,7 +1233,11 @@
   @Test
   @TestProjectInput(cloneAs = "user")
   public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception {
-    allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     try {
       PushOneCommit.Result changeResult =
@@ -1193,7 +1251,11 @@
           assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
       assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
-      removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
+          .update();
     }
   }
 
@@ -1474,11 +1536,12 @@
   public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // admin pushes commit of user
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1494,12 +1557,8 @@
 
     // check the user cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // check that the author/committer was NOT added as reviewer (he can't see
     // the change)
@@ -1547,11 +1606,12 @@
   public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // admin pushes commit that references 'user' in a footer
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1571,12 +1631,8 @@
 
     // check that 'user' cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // check that 'user' was NOT added as cc ('user' can't see the change)
     requestScopeOperations.setApiUser(admin.id());
@@ -1590,11 +1646,12 @@
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // create change
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1604,12 +1661,8 @@
 
     // check the user cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.changes().id(result.getChangeId()).get();
-      fail("Expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      // Expected.
-    }
+    assertThrows(
+        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
 
     // try to add user as reviewer
     requestScopeOperations.setApiUser(admin.id());
@@ -2101,21 +2154,21 @@
 
   @Test
   public void removeReviewerNoVotes() throws Exception {
+    LabelType verified =
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType verified =
-          category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      String heads = RefNames.REFS_HEADS + "*";
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.verified().getName()),
-          -1,
-          1,
-          registeredUsers,
-          heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(verified.getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
 
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
@@ -2388,16 +2441,18 @@
   @Test
   public void nonVotingReviewerStaysAfterSubmit() throws Exception {
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+    String heads = "refs/heads/*";
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      String heads = "refs/heads/*";
-      AccountGroup.UUID owners = systemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
-      AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, owners, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, registered, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(CHANGE_OWNER).range(-1, 1))
+        .add(allowLabel("Code-Review").ref(heads).group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     // Set Code-Review+2 and Verified+1 as admin (change owner)
     PushOneCommit.Result r = createChange();
@@ -2493,8 +2548,13 @@
 
   @Test
   public void queryChangesNoLimit() throws Exception {
-    allowGlobalCapabilities(
-        SystemGroupBackend.REGISTERED_USERS, 0, 2, GlobalCapability.QUERY_LIMIT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.QUERY_LIMIT)
+                .group(SystemGroupBackend.REGISTERED_USERS)
+                .range(0, 2))
+        .update();
     for (int i = 0; i < 3; i++) {
       createChange();
     }
@@ -2642,7 +2702,11 @@
   public void editTopicWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
-    grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_TOPIC_NAME).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).topic("mytopic");
     assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
@@ -2699,7 +2763,11 @@
   public void submitAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    grant(project, "refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
     assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
@@ -2715,22 +2783,24 @@
   @Test
   public void commitFooters() throws Exception {
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     LabelType custom1 =
-        category("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+        label("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     LabelType custom2 =
-        category("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+        label("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
       u.getConfig().getLabelSections().put(custom1.getName(), custom1);
       u.getConfig().getLabelSections().put(custom2.getName(), custom2);
-      String heads = "refs/heads/*";
-      AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.forLabel("Verified"), -1, 1, anon, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Custom1"), -1, 1, anon, heads);
-      Util.allow(u.getConfig(), Permission.forLabel("Custom2"), -1, 1, anon, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(custom1.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(custom2.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
 
     PushOneCommit.Result r1 = createChange();
     r1.assertOkStatus();
@@ -2853,10 +2923,11 @@
     assertThat(approval._accountId).isEqualTo(user.id().get());
     assertThat(approval.value).isEqualTo(0);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
@@ -2974,7 +3045,11 @@
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as admin
     PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
@@ -3018,7 +3093,11 @@
     TestRepository<?> adminTestRepo = cloneProject(project, admin);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as admin
     PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
@@ -3112,7 +3191,7 @@
 
   @Test
   public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     createBranch("foo");
     createBranch("bar");
 
@@ -3141,7 +3220,7 @@
 
   @Test
   public void createMergePatchSetBaseOnChange() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     createBranch("foo");
     createBranch("bar");
 
@@ -3189,15 +3268,18 @@
 
     // add new label and assert that it's returned for existing changes
     AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    LabelType verified = Util.verified();
+    LabelType verified = TestLabels.verified();
     String heads = RefNames.REFS_HEADS + "*";
 
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      Util.allow(
-          u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -3215,9 +3297,16 @@
       // remove label and assert that it's no longer returned for existing
       // changes, even if there is an approval for it
       u.getConfig().getLabelSections().remove(verified.getName());
-      Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(
+            permissionKey(Permission.forLabel(verified.getName()))
+                .ref(heads)
+                .group(registeredUsers))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -3244,17 +3333,20 @@
     assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review");
     assertPermitted(change, "Code-Review", 2);
 
-    LabelType verified = Util.verified();
+    LabelType verified = TestLabels.verified();
     AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
     String heads = RefNames.REFS_HEADS + "*";
 
     // add new label and assert that it's returned for existing changes
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().put(verified.getName(), verified);
-      Util.allow(
-          u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -3296,9 +3388,13 @@
     // changes, even if there is an approval for it
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getLabelSections().remove(verified.getName());
-      Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(verified.getName()).ref(heads).group(registeredUsers))
+        .update();
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -3309,7 +3405,7 @@
   @Test
   public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception {
     // Configure Non-Author-Code-Review
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
     testRepo.reset("config");
     PushOneCommit push2 =
@@ -3334,20 +3430,18 @@
     push2.to(RefNames.REFS_CONFIG);
     testRepo.reset(oldHead);
 
-    AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
     String heads = RefNames.REFS_HEADS + "*";
 
     // Allow user to approve
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          registeredUsers,
-          heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
 
     PushOneCommit.Result r = createChange();
 
@@ -3399,16 +3493,15 @@
     assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
     assertThat(approval.permittedVotingRange.max).isEqualTo(1);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel("Code-Review"),
-          minPermittedValue,
-          maxPermittedValue,
-          REGISTERED_USERS,
-          heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(minPermittedValue, maxPermittedValue))
+        .update();
 
     c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
@@ -3422,10 +3515,11 @@
 
   @Test
   public void maxPermittedValueBlocked() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3504,7 +3598,7 @@
             + "U > 0,"
             + "R = label('All-Comments-Resolved', need(_)). \n\n");
 
-    String oldHead = getRemoteHead().name();
+    String oldHead = projectOperations.project(project).getHead("master").name();
     PushOneCommit.Result result1 =
         pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
@@ -3685,7 +3779,11 @@
     Project.NameKey p = projectOperations.newProject().create();
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
     // Create change as user
     PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
@@ -3756,7 +3854,8 @@
     assertThat(
             gApi.changes()
                 .id(revertId)
-                .pureRevert(getRemoteHead().toObjectId().name())
+                .pureRevert(
+                    projectOperations.project(project).getHead("master").toObjectId().name())
                 .isPureRevert)
         .isTrue();
   }
@@ -3779,7 +3878,7 @@
   public void pureRevertParameterTakesPrecedence() throws Exception {
     PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
     merge(r1);
-    String oldHead = getRemoteHead().toObjectId().name();
+    String oldHead = projectOperations.project(project).getHead("master").toObjectId().name();
 
     PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
     merge(r2);
@@ -3822,7 +3921,8 @@
     // Create an initial commit to serve as claimed original
     PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
     merge(r1);
-    String claimedOriginal = getRemoteHead().toObjectId().name();
+    String claimedOriginal =
+        projectOperations.project(project).getHead("master").toObjectId().name();
 
     // Change contents of the file to provoke a conflict
     merge(createChange("commit message", "a.txt", "content2"));
@@ -3874,13 +3974,13 @@
 
   public void submittableAfterLosingPermissions(String label) throws Exception {
     String codeReviewLabel = "Code-Review";
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel(label), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(codeReviewLabel), -2, +2, registered, "refs/heads/*");
-      u.save();
-    }
+    AccountGroup.UUID registered = REGISTERED_USERS;
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label).ref("refs/heads/*").group(registered).range(-1, +1))
+        .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-2, +2))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     PushOneCommit.Result r = createChange();
@@ -3903,15 +4003,13 @@
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
     requestScopeOperations.setApiUser(admin.id());
-    // Remove user's permission for 'Label'.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
-      // Update user's permitted range for 'Code-Review' to be -1...+1.
-      Util.remove(u.getConfig(), Permission.forLabel(codeReviewLabel), registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(codeReviewLabel), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(labelPermissionKey(label).ref("refs/heads/*").group(registered))
+        .remove(labelPermissionKey(codeReviewLabel).ref("refs/heads/*").group(registered))
+        .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-1, +1))
+        .update();
 
     // Verify user's new permitted range.
     requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
index eab6c3c..57d7ece 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/DisablePrivateChangesIT.java
@@ -20,12 +20,15 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class DisablePrivateChangesIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
@@ -63,7 +66,7 @@
   @GerritConfig(name = "change.allowDrafts", value = "true")
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
   public void pushDraftsWithDisablePrivateChangesTrue() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result result =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     result.assertErrorStatus();
@@ -93,7 +96,7 @@
   @Test
   @GerritConfig(name = "change.allowDrafts", value = "true")
   public void pushDraftsWithDisablePrivateChangesFalse() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result result =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
     assertThat(result.getChange().change().isPrivate()).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index ab72491..59237cb 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -42,6 +44,7 @@
 
 public class PrivateChangeIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -167,7 +170,11 @@
     PushOneCommit.Result result = createChange();
     gApi.changes().id(result.getChangeId()).setPrivate(true, null);
 
-    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 5052b15..99935b5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CHANGE;
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CODE_CHANGE;
@@ -25,8 +27,8 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.collect.ImmutableList;
@@ -35,9 +37,9 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -45,9 +47,8 @@
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import java.util.EnumSet;
 import java.util.List;
@@ -61,6 +62,7 @@
 
 @NoHttpd
 public class StickyApprovalsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -69,7 +71,7 @@
       // Overwrite "Code-Review" label that is inherited from All-Projects.
       // This way changes to the "Code Review" label don't affect other tests.
       LabelType codeReview =
-          category(
+          label(
               "Code-Review",
               value(2, "Looks good to me, approved"),
               value(1, "Looks good to me, but someone else must approve"),
@@ -80,28 +82,26 @@
       u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
 
       LabelType verified =
-          category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+          label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
       verified.setCopyAllScoresIfNoChange(false);
       u.getConfig().getLabelSections().put(verified.getName(), verified);
 
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      String heads = RefNames.REFS_HEADS + "*";
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          registeredUsers,
-          heads);
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.verified().getName()),
-          -1,
-          1,
-          registeredUsers,
-          heads);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .add(
+            allowLabel(TestLabels.verified().getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
   }
 
   @Test
@@ -119,7 +119,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, -1, 1);
@@ -141,7 +141,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, 2, 1);
@@ -178,7 +178,7 @@
     assertNotSticky(EnumSet.of(REWORK, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE));
 
     // check that votes are sticky when trivial rebase is done by cherry-pick
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     changeId = createChange().getChangeId();
     vote(admin, changeId, 2, 1);
     vote(user, changeId, -2, -1);
@@ -189,7 +189,7 @@
     assertVotes(c, user, -2, 0);
 
     // check that votes are not sticky when rework is done by cherry-pick
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     changeId = createChange().getChangeId();
     vote(admin, changeId, 2, 1);
     vote(user, changeId, -2, -1);
@@ -261,7 +261,7 @@
 
     for (ChangeKind changeKind :
         EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, 2, 1);
@@ -369,7 +369,7 @@
 
   private void assertNotSticky(Set<ChangeKind> changeKinds) throws Exception {
     for (ChangeKind changeKind : changeKinds) {
-      testRepo.reset(getRemoteHead());
+      testRepo.reset(projectOperations.project(project).getHead("master"));
 
       String changeId = createChange(changeKind);
       vote(admin, changeId, +2, 1);
@@ -414,7 +414,7 @@
         noChange(changeId);
         return;
       default:
-        fail("unexpected change kind: " + changeKind);
+        assert_().fail("unexpected change kind: " + changeKind);
     }
   }
 
@@ -460,7 +460,7 @@
 
   private void trivialRebase(String changeId) throws Exception {
     requestScopeOperations.setApiUser(admin.id());
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     PushOneCommit push =
         pushFactory.create(
             admin.newIdent(),
@@ -523,10 +523,10 @@
       case NO_CHANGE:
       case MERGE_FIRST_PARENT_UPDATE:
       default:
-        fail("unexpected change kind: " + changeKind);
+        assert_().fail("unexpected change kind: " + changeKind);
     }
 
-    testRepo.reset(getRemoteHead());
+    testRepo.reset(projectOperations.project(project).getHead("master"));
     PushOneCommit.Result r =
         pushFactory
             .create(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 6c0e9ab..62600b2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
 import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS;
 import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -237,22 +238,21 @@
     gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
     gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
 
-    try {
-      gApi.changes().id(r2.getChangeId()).current().submit();
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Failed to submit 2 changes due to the following problems:\n"
-                  + "Change "
-                  + r1.getChange().getId()
-                  + ": Change has submit type "
-                  + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
-                  + "from change "
-                  + r2.getChange().getId()
-                  + " in the same batch");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(r2.getChangeId()).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to submit 2 changes due to the following problems:\n"
+                + "Change "
+                + r1.getChange().getId()
+                + ": Change has submit type "
+                + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
+                + "from change "
+                + r2.getChange().getId()
+                + " in the same batch");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index cdcfc0c..ab1dbd3 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -51,6 +54,7 @@
 public class GroupsConsistencyIT extends AbstractDaemonTest {
 
   @Inject protected GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   private GroupInfo gAdmin;
   private GroupInfo g1;
   private GroupInfo g2;
@@ -59,7 +63,10 @@
 
   @Before
   public void basicSetup() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     String name1 = groupOperations.newGroup().name("g1").create().get();
     String name2 = groupOperations.newGroup().name("g2").create().get();
@@ -245,7 +252,7 @@
       }
     }
 
-    fail(String.format("could not find %s substring '%s' in %s", want, msg, problems));
+    assert_().fail(String.format("could not find %s substring '%s' in %s", want, msg, problems));
   }
 
   private void updateGroupFile(String refName, String fileName, String content) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 28ebadb..73e3982 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -22,6 +22,9 @@
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -44,6 +47,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -86,8 +90,6 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.StalenessChecker;
 import com.google.gerrit.server.notedb.Sequences;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
@@ -133,6 +135,7 @@
   @Inject private Groups groups;
   @Inject private GroupsConsistencyChecker consistencyChecker;
   @Inject private PeriodicGroupIndexer slaveGroupIndexer;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Sequences seq;
   @Inject private StalenessChecker stalenessChecker;
@@ -999,11 +1002,7 @@
     gApi.groups().id(uuid.get()).index();
 
     // Verify "sub-group" has been deleted.
-    try {
-      gApi.groups().id(uuid.get()).get();
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-    }
+    assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id(uuid.get()).get());
   }
 
   // reindex is tested by {@link AbstractQueryGroupsTest#reindex}
@@ -1048,7 +1047,10 @@
   @Test
   public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
     // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, "group update not allowed");
   }
 
@@ -1078,15 +1080,18 @@
 
   private void assertPushToGroupBranch(
       Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_DELETED_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPNAMES);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(
+            allow(Permission.CREATE)
+                .ref(RefNames.REFS_DELETED_GROUPS + "*")
+                .group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_DELETED_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPNAMES).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
 
@@ -1105,12 +1110,12 @@
   }
 
   private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.CREATE, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, RefNames.REFS_GROUPS + "*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     PushOneCommit.Result r =
         pushFactory
@@ -1191,15 +1196,22 @@
       }
 
       // refs/meta/group-names is only visible with ACCESS_DATABASE
-      allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+      projectOperations
+          .allProjectsForUpdate()
+          .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+          .update();
 
       testCannotCreateGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
     }
   }
 
   private void testCannotCreateGroupBranch(String refPattern, String groupRef) throws Exception {
-    grant(allUsers, refPattern, Permission.CREATE);
-    grant(allUsers, refPattern, Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(refPattern).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(refPattern).group(adminGroupUuid()))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     PushOneCommit.Result r = pushFactory.create(admin.newIdent(), allUsersRepo).to(groupRef);
@@ -1226,13 +1238,20 @@
   @Test
   public void cannotDeleteGroupNamesBranch() throws Exception {
     // refs/meta/group-names is only visible with ACCESS_DATABASE
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     testCannotDeleteGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
   }
 
   private void testCannotDeleteGroupBranch(String refPattern, String groupRef) throws Exception {
-    grant(allUsers, refPattern, Permission.DELETE, true, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref(refPattern).group(REGISTERED_USERS).force(true))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     PushResult r = deleteRef(allUsersRepo, groupRef);
@@ -1415,8 +1434,16 @@
 
   private void pushToGroupBranchForReviewAndSubmit(
       Project.NameKey project, String groupRef, String expectedError) throws Throwable {
-    grantLabel("Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", REGISTERED_USERS, false);
-    grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref(RefNames.REFS_GROUPS + "*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
     fetch(repo, groupRef + ":groupRef");
@@ -1517,12 +1544,7 @@
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 
   @Target({METHOD})
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 2943445..e8af0bb 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -118,12 +118,7 @@
 
     // Non-admin cannot disable
     requestScopeOperations.setApiUser(user.id());
-    try {
-      gApi.plugins().name("plugin-a").disable();
-      fail("Expected AuthException");
-    } catch (AuthException expected) {
-      // Expected
-    }
+    assertThrows(AuthException.class, () -> gApi.plugins().name("plugin-a").disable());
   }
 
   @SuppressWarnings("deprecation")
@@ -172,11 +167,6 @@
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 4bc4037..3fcc595 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -15,6 +15,10 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
@@ -33,8 +37,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -63,29 +65,32 @@
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
     groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
 
-    try (ProjectConfigUpdate u = updateProject(secretProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/*");
-      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(secretProject)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(privilegedGroupUuid))
+        .add(block(Permission.READ).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
-    try (ProjectConfigUpdate u = updateProject(secretRefProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.deny(cfg, Permission.READ, SystemGroupBackend.ANONYMOUS_USERS, "refs/*");
-      Util.allow(cfg, Permission.READ, privilegedGroupUuid, "refs/heads/secret/*");
-      Util.block(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/secret/*");
-      Util.allow(cfg, Permission.READ, SystemGroupBackend.REGISTERED_USERS, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(secretRefProject)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/secret/*").group(privilegedGroupUuid))
+        .add(
+            block(Permission.READ)
+                .ref("refs/heads/secret/*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
     // Ref permission
-    try (ProjectConfigUpdate u = updateProject(normalProject)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.VIEW_PRIVATE_CHANGES, privilegedGroupUuid, "refs/*");
-      Util.allow(cfg, Permission.FORGE_SERVER, privilegedGroupUuid, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(normalProject)
+        .forUpdate()
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(privilegedGroupUuid))
+        .add(allow(Permission.FORGE_SERVER).ref("refs/*").group(privilegedGroupUuid))
+        .update();
   }
 
   @Test
@@ -242,13 +247,15 @@
       try {
         info = gApi.projects().name(tc.project).checkAccess(tc.input);
       } catch (RestApiException e) {
-        fail(String.format("check.access(%s, %s): exception %s", tc.project, in, e));
+        assert_().fail(String.format("check.access(%s, %s): exception %s", tc.project, in, e));
       }
 
       int want = tc.want;
       if (want != info.status) {
-        fail(
-            String.format("check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want));
+        assert_()
+            .fail(
+                String.format(
+                    "check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want));
       }
 
       switch (want) {
@@ -264,7 +271,7 @@
           assertThat(info.message).isNull();
           break;
         default:
-          fail(String.format("unknown code %d", want));
+          assert_().fail(String.format("unknown code %d", want));
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIncludedInIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIncludedInIT.java
index 54aa192..749176b8f 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIncludedInIT.java
@@ -15,21 +15,26 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
 @NoHttpd
 public class CommitIncludedInIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void includedInOpenChange() throws Exception {
     Result result = createChange();
@@ -49,7 +54,11 @@
     assertThat(getIncludedIn(result.getCommit().getId()).branches).containsExactly("master");
     assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty();
 
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(result.getChange().project().get()).tag("test-tag").create(new TagInput());
 
     assertThat(getIncludedIn(result.getCommit().getId()).tags).containsExactly("test-tag");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
index 4a5ad6a..7ff6207 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
@@ -22,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
@@ -32,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.restapi.project.DashboardsCollection;
+import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
@@ -41,9 +44,15 @@
 
 @NoHttpd
 public class DashboardIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Before
   public void setup() throws Exception {
-    allow("refs/meta/dashboards/*", Permission.CREATE, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/meta/dashboards/*").group(REGISTERED_USERS))
+        .update();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index a516468..dce0ab2 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_GLOBAL;
 import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_PARENT;
@@ -238,7 +240,11 @@
 
   @Test
   public void createAndDeleteBranchByPush() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     projectIndexedCounter.clear();
 
     assertThat(hasHead(project, "foo")).isFalse();
@@ -256,14 +262,14 @@
 
   @Test
   public void descriptionChangeCausesRefUpdate() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     assertThat(gApi.projects().name(project.get()).description()).isEmpty();
     DescriptionInput in = new DescriptionInput();
     in.description = "new project description";
     gApi.projects().name(project.get()).description(in);
     assertThat(gApi.projects().name(project.get()).description()).isEqualTo(in.description);
 
-    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         project.get(), RefNames.REFS_CONFIG, initialHead, updatedHead);
   }
@@ -282,7 +288,7 @@
 
   @Test
   public void configChangeCausesRefUpdate() throws Exception {
-    RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
 
     ConfigInfo info = gApi.projects().name(project.get()).config();
     assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
@@ -293,7 +299,7 @@
     info = gApi.projects().name(project.get()).config();
     assertThat(info.defaultSubmitType.value).isEqualTo(SubmitType.CHERRY_PICK);
 
-    RevCommit updatedHead = getRemoteHead(project, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         project.get(), RefNames.REFS_CONFIG, initialHead, updatedHead);
   }
@@ -425,8 +431,18 @@
     assertThat(gApi.projects().name(project.get()).config().state).isEqualTo(ProjectState.HIDDEN);
 
     // Revoke OWNER permission for admin and block them from reading the project's refs
-    block(project, RefNames.REFS + "*", Permission.OWNER, SystemGroupBackend.REGISTERED_USERS);
-    block(project, RefNames.REFS + "*", Permission.READ, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            block(Permission.OWNER)
+                .ref(RefNames.REFS + "*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .add(
+            block(Permission.READ)
+                .ref(RefNames.REFS + "*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
 
     // HIDDEN => ACTIVE
     ConfigInput ci2 = new ConfigInput();
@@ -725,7 +741,7 @@
 
   @Nullable
   protected RevCommit getRemoteHead(String project, String branch) throws Exception {
-    return getRemoteHead(Project.nameKey(project), branch);
+    return projectOperations.project(Project.nameKey(project)).getHead(branch);
   }
 
   boolean hasHead(Project.NameKey k, String b) {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 6b511f6..5892536 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -117,15 +118,15 @@
 
   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");
+    assertThrows(
+        UnsupportedOperationException.class,
+        () -> {
+          try (AutoCloseable ignored = disableProjectIndex()) {
+            try (ProjectConfigUpdate u = updateProject(project)) {
+              update.accept(u.getConfig());
+              u.save();
+            }
+          }
+        });
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index b2da402..82eef1d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -35,7 +36,6 @@
 
 @NoHttpd
 public class SetParentIT extends AbstractDaemonTest {
-
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
@@ -74,7 +74,11 @@
   public void setParentAllowedForOwners() throws Exception {
     String parent = projectOperations.newProject().create().get();
     requestScopeOperations.setApiUser(user.id());
-    grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index bd5a1f7..677fece 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
@@ -43,6 +44,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -115,10 +117,10 @@
 import org.junit.Test;
 
 public class RevisionIT extends AbstractDaemonTest {
-
   @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
   @Inject private DynamicSet<PatchSetWebLink> patchSetLinks;
   @Inject private GetRevisionActions getRevisionActions;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -182,14 +184,13 @@
     assertThat(approval.postSubmit).isNull();
 
     // Reducing vote is not allowed.
-    try {
-      gApi.changes().id(changeId).current().review(ReviewInput.dislike());
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(1);
     assertThat(approval.postSubmit).isNull();
@@ -202,14 +203,13 @@
     assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 2);
 
     // Decreasing to previous post-submit vote is still not allowed.
-    try {
-      gApi.changes().id(changeId).current().review(ReviewInput.dislike());
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
-    }
+    thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(2);
     assertThat(approval.postSubmit).isTrue();
@@ -536,12 +536,11 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = destBranch;
     in.message = "Cherry-Pick";
-    try {
-      changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: merge conflict");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in));
+    assertThat(thrown).hasMessageThat().isEqualTo("Cherry pick failed: merge conflict");
 
     // Cherry-pick with auto merge should succeed.
     in.allowConflicts = true;
@@ -565,7 +564,7 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String destSha1 = abbreviateName(getRemoteHead(project, destBranch), 6);
+    String destSha1 = abbreviateName(projectOperations.project(project).getHead(destBranch), 6);
     String changeSha1 = abbreviateName(r.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
@@ -618,16 +617,15 @@
     CherryPickInput in = new CherryPickInput();
     in.destination = "foo";
     in.message = r1.getCommit().getFullMessage();
-    try {
-      gApi.changes().id(t1).current().cherryPick(in);
-      fail("expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "Cannot create new patch set of change "
-                  + info(t2)._number
-                  + " because it is abandoned");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(t1).current().cherryPick(in));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Cannot create new patch set of change "
+                + info(t2)._number
+                + " because it is abandoned");
 
     gApi.changes().id(t2).restore();
     gApi.changes().id(t1).current().cherryPick(in);
@@ -1187,7 +1185,11 @@
   public void setDescriptionAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     assertDescription(r, "");
-    grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
     assertDescription(r, "test");
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 28d94d3..070b98c 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
@@ -58,7 +59,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Post;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Put;
@@ -600,7 +601,7 @@
   public void editCommitMessageCopiesLabelScores() throws Exception {
     String cr = "Code-Review";
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReview = Util.codeReview();
+      LabelType codeReview = TestLabels.codeReview();
       codeReview.setCopyAllScoresIfNoCodeChange(true);
       u.getConfig().getLabelSections().put(cr, codeReview);
       u.save();
@@ -693,7 +694,11 @@
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
     // Block default permission
-    block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
+        .update();
 
     // Create change as user
     PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index bfbe3a3..0714cca 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -24,6 +24,10 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.GitUtil.pushOne;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
@@ -34,8 +38,8 @@
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.util.Comparator.comparing;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.joining;
@@ -54,6 +58,7 @@
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
@@ -93,7 +98,7 @@
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.TestTimeUtil;
@@ -131,11 +136,17 @@
 @SkipProjectClone
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
-    // TODO(dborowitz): TEST.
+    // Only test protocols which are actually served by the Gerrit server, since each separate test
+    // class is large and slow.
+    //
+    // This list excludes the test InProcessProtocol, which is used by large numbers of other
+    // acceptance tests. Small tests of InProcessProtocol are still possible, without incurring a
+    // new large slow test.
     SSH,
     HTTP
   }
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private static String NEW_CHANGE_INDICATOR = " [NEW]";
@@ -154,19 +165,24 @@
   @Before
   public void setUpPatchSetLock() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      patchSetLock = Util.patchSetLock();
+      patchSetLock = TestLabels.patchSetLock();
       u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(patchSetLock.getName()),
-          0,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
       u.save();
     }
-    grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(ANONYMOUS_USERS)
+                .range(0, 1))
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(adminGroupUuid())
+                .range(0, 1))
+        .update();
   }
 
   @After
@@ -871,8 +887,14 @@
 
     // Non owner, non admin and non project owner cannot flip wip bit:
     TestAccount user2 = accountCreator.user2();
-    grant(
-        project, "refs/*", Permission.FORGE_COMMITTER, false, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allow(Permission.FORGE_COMMITTER)
+                .ref("refs/*")
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     TestRepository<?> user2Repo = cloneProject(project, user2);
     GitUtil.fetch(user2Repo, r.getPatchSet().refName() + ":ps");
     user2Repo.reset("ps");
@@ -880,7 +902,11 @@
     r.assertErrorStatus(ReceiveConstants.ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
 
     // Project owner trying to move from WIP to ready should succeed.
-    allow("refs/*", Permission.OWNER, SystemGroupBackend.REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
     r.assertOkStatus();
   }
@@ -1181,14 +1207,17 @@
   @Test
   public void pushWithMultipleApprovals() throws Exception {
     LabelType Q =
-        category("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
-    AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+        label("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
     String heads = "refs/heads/*";
     try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel("Custom-Label"), -1, 1, anon, heads);
       u.getConfig().getLabelSections().put(Q.getName(), Q);
       u.save();
     }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Custom-Label").ref(heads).group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
 
     RevCommit c =
         commitBuilder()
@@ -1349,7 +1378,11 @@
     r.assertOkStatus();
 
     setUseSignedOffBy(InheritableBoolean.TRUE);
-    block(project, "refs/heads/master", Permission.FORGE_COMMITTER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.FORGE_COMMITTER).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     push =
         pushFactory.create(
@@ -1419,7 +1452,11 @@
 
   @Test
   public void pushSameCommitTwiceUsingMagicBranchBaseOption() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result rBase = pushTo("refs/heads/master");
     rBase.assertOkStatus();
 
@@ -1833,7 +1870,11 @@
 
   @Test
   public void forcePushAbandonedChange() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     PushOneCommit push1 =
         pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r = push1.to("refs/for/master");
@@ -1906,7 +1947,7 @@
   @Test
   public void pushNewPatchsetOverridingStickyLabel() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReview = Util.codeReview();
+      LabelType codeReview = TestLabels.codeReview();
       codeReview.setCopyMaxScore(true);
       u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
       u.save();
@@ -1929,7 +1970,11 @@
   @Test
   public void createChangeForMergedCommit() throws Exception {
     String master = "refs/heads/master";
-    grant(project, master, Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
+        .update();
 
     // Update master with a direct push.
     RevCommit c1 = testRepo.commit().message("Non-change 1").create();
@@ -2028,7 +2073,11 @@
   @Test
   public void mergedOptionWithExistingChangeInsertsPatchSet() throws Exception {
     String master = "refs/heads/master";
-    grant(project, master, Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
+        .update();
 
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
@@ -2297,12 +2346,19 @@
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushRejected(pr, ref, "NoteDb update requires access database permission");
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushRejected(pr, ref, "prohibited by Gerrit: not permitted: create");
 
-    grant(project, "refs/changes/*", Permission.CREATE);
-    grant(project, "refs/changes/*", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/changes/*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref("refs/changes/*").group(adminGroupUuid()))
+        .update();
     grantSkipValidation(project, "refs/changes/*", REGISTERED_USERS);
     pr = pushOne(testRepo, c.name(), ref, false, false, opts);
     assertPushOk(pr, ref);
@@ -2668,13 +2724,14 @@
   private void grantSkipValidation(Project.NameKey project, String ref, AccountGroup.UUID groupUuid)
       throws Exception {
     // See SKIP_VALIDATION implementation in default permission backend.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.FORGE_AUTHOR, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.FORGE_COMMITTER, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.FORGE_SERVER, groupUuid, ref);
-      Util.allow(u.getConfig(), Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.FORGE_AUTHOR).ref(ref).group(groupUuid))
+        .add(allow(Permission.FORGE_COMMITTER).ref(ref).group(groupUuid))
+        .add(allow(Permission.FORGE_SERVER).ref(ref).group(groupUuid))
+        .add(allow(Permission.PUSH_MERGE).ref("refs/for/" + ref).group(groupUuid))
+        .update();
   }
 
   private PushOneCommit.Result amendChange(String changeId, String ref) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index fc2e5cb..c35b891 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -59,11 +60,12 @@
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   protected TestRepository<?> superRepo;
   protected Project.NameKey superKey;
   protected TestRepository<?> subRepo;
   protected Project.NameKey subKey;
-  @Inject protected ProjectOperations projectOperations;
 
   protected SubmitType getSubmitType() {
     return cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
@@ -105,8 +107,12 @@
   }
 
   protected void grantPush(Project.NameKey project) throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH);
-    grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+        .update();
   }
 
   protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index b895ddf..5ec7b0e 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK;
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
@@ -23,8 +24,10 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.transport.PushResult;
@@ -33,6 +36,7 @@
 
 @NoHttpd
 public class ForcePushIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void forcePushNotAllowed() throws Exception {
@@ -57,7 +61,11 @@
   @Test
   public void forcePushAllowed() throws Exception {
     ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     PushOneCommit push1 =
         pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
     PushOneCommit.Result r1 = push1.to("refs/heads/master");
@@ -82,19 +90,31 @@
 
   @Test
   public void deleteNotAllowedWithOnlyPushPermission() throws Exception {
-    grant(project, "refs/*", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()))
+        .update();
     assertDeleteRef(REJECTED_OTHER_REASON);
   }
 
   @Test
   public void deleteAllowedWithForcePushPermission() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     assertDeleteRef(OK);
   }
 
   @Test
   public void deleteAllowedWithDeletePermission() throws Exception {
-    grant(project, "refs/*", Permission.DELETE, true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid()).force(true))
+        .update();
     assertDeleteRef(OK);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index f682342..dd12cd3 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.git.testing.PushResultSubject.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
@@ -24,6 +25,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -36,7 +38,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.function.Consumer;
@@ -54,6 +55,7 @@
 import org.junit.Test;
 
 public class PushPermissionsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -73,13 +75,15 @@
           Permission.PUSH_MERGE,
           Permission.SUBMIT);
       removeAllGlobalCapabilities(cfg, GlobalCapability.ADMINISTRATE_SERVER);
-
-      // Include some auxiliary permissions.
-      Util.allow(cfg, Permission.FORGE_AUTHOR, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, "refs/*");
-
       u.save();
     }
+
+    // Include some auxiliary permissions.
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
   @Test
@@ -202,7 +206,11 @@
 
   @Test
   public void refsMetaConfigUpdateRequiresProjectOwner() throws Exception {
-    grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS))
+        .update();
 
     forceFetch("refs/meta/config");
     ObjectId commit = testRepo.branch("refs/meta/config").commit().create();
@@ -222,7 +230,11 @@
             "Contact an administrator to fix the permissions");
     assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
 
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Re-fetch refs/meta/config from the server because the grant changed it, and we want a
     // fast-forward.
@@ -249,7 +261,11 @@
 
   @Test
   public void updateBySubmitDenied() throws Exception {
-    grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     ObjectId commit = testRepo.branch("HEAD").commit().create();
     assertThat(push("HEAD:refs/for/master")).onlyRef("refs/for/master").isOk();
@@ -267,7 +283,11 @@
 
   @Test
   public void addPatchSetDenied() throws Exception {
-    grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput ci = new ChangeInput();
     ci.project = project.get();
@@ -288,7 +308,11 @@
 
   @Test
   public void skipValidationDenied() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     testRepo.branch("HEAD").commit().create();
     PushResult r =
@@ -305,7 +329,11 @@
 
   @Test
   public void accessDatabaseForNoteDbDenied() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     testRepo.branch("HEAD").commit().create();
     PushResult r =
@@ -322,8 +350,12 @@
 
   @Test
   public void administrateServerForUpdateParentDenied() throws Exception {
-    grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
-    grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS))
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     String project2 = name("project2");
     gApi.projects().create(project2);
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 4e37a7c..1c4547b 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -18,6 +18,10 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
@@ -29,6 +33,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
@@ -50,7 +55,6 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -78,6 +82,7 @@
   @Inject private AllUsersName allUsersName;
   @Inject private ChangeNoteUtil noteUtil;
   @Inject private PermissionBackend permissionBackend;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private AccountGroup.UUID admins;
@@ -121,9 +126,12 @@
       for (AccessSection sec : u.getConfig().getAccessSections()) {
         sec.removePermission(Permission.READ);
       }
-      Util.allow(u.getConfig(), Permission.READ, admins, "refs/*");
       u.save();
     }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(admins))
+        .update();
 
     // Remove all read permissions on All-Users.
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
@@ -139,7 +147,11 @@
 
     // First 2 changes are merged, which means the tags pointing to them are
     // visible.
-    allow("refs/for/refs/heads/*", Permission.SUBMIT, admins);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(admins))
+        .update();
     PushOneCommit.Result mr =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%submit");
     mr.assertOkStatus();
@@ -182,12 +194,13 @@
 
   @Test
   public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.READ, admins, RefNames.REFS_CONFIG);
-      Util.doNotInherit(u.getConfig(), Permission.READ, RefNames.REFS_CONFIG);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(admins))
+        .setExclusiveGroup(permissionKey(Permission.READ).ref(RefNames.REFS_CONFIG), true)
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -208,8 +221,12 @@
 
   @Test
   public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
-    allow(RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
 
     assertUploadPackRefs(
         "HEAD",
@@ -230,8 +247,12 @@
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(deny(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -240,8 +261,12 @@
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception {
-    deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -258,7 +283,11 @@
 
   @Test
   public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     // Admin's edit is not visible.
     requestScopeOperations.setApiUser(admin.id());
@@ -281,8 +310,12 @@
 
   @Test
   public void uploadPackSubsetOfBranchesAndEditsVisibleWithViewPrivateChanges() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Admin's edit on change3 is visible.
     requestScopeOperations.setApiUser(admin.id());
@@ -309,9 +342,16 @@
 
   @Test
   public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
-    deny("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    allow("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(deny(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(admin.id());
     gApi.changes().id(cd3.getId().get()).edit().create();
@@ -348,7 +388,11 @@
   }
 
   private void uploadPackNoSearchingChangeCacheImpl() throws Exception {
-    allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     assertRefs(
@@ -375,13 +419,20 @@
   public void uploadPackSequencesWithAccessDatabase() throws Exception {
     assertRefs(allProjects, user, true);
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     assertRefs(allProjects, user, true, "refs/sequences/changes");
   }
 
   @Test
   public void uploadPackAllRefsAreVisibleOrphanedTag() throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     // Delete the pending change on 'branch' and 'branch' itself so that the tag gets orphaned
     gApi.changes().id(cd4.getId().get()).delete();
     gApi.projects().name(project.get()).branch("refs/heads/branch").delete();
@@ -418,8 +469,12 @@
 
   @Test
   public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
-    deny("refs/heads/branch", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(deny(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
 
     assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1));
@@ -482,7 +537,11 @@
 
   @Test
   public void advertisedReferencesOmitUserBranchesOfOtherUsers() throws Exception {
-    allow(allUsersName, RefNames.REFS_USERS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
@@ -492,7 +551,10 @@
 
   @Test
   public void advertisedReferencesIncludeAllUserBranchesWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getUserRefs(git))
@@ -514,7 +576,11 @@
 
   @Test
   public void advertisedReferencesOmitGroupBranchesOfNonOwnedGroups() throws Exception {
-    allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins, user);
     AccountGroup.UUID foos = createGroup("Foos", users);
     AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
@@ -527,7 +593,10 @@
 
   @Test
   public void advertisedReferencesIncludeAllGroupBranchesWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins);
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
@@ -541,8 +610,15 @@
 
   @Test
   public void advertisedReferencesIncludeAllGroupBranchesForAdmins() throws Exception {
-    allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
     AccountGroup.UUID users = createGroup("Users", admins);
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
@@ -556,7 +632,11 @@
 
   @Test
   public void advertisedReferencesOmitNoteDbNotesBranches() throws Exception {
-    allow(allUsersName, RefNames.REFS + "*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS + "*").group(REGISTERED_USERS))
+        .update();
     TestRepository<?> userTestRepository = cloneProject(allUsers, user);
     try (Git git = userTestRepository.git()) {
       assertThat(getRefs(git)).containsNoneOf(RefNames.REFS_EXTERNAL_IDS, RefNames.REFS_GROUPNAMES);
@@ -565,7 +645,11 @@
 
   @Test
   public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception {
-    allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
@@ -582,7 +666,11 @@
     assume()
         .that(baseConfig.getBoolean("auth", "skipFullRefEvaluationIfAllRefsAreVisible", true))
         .isTrue();
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
@@ -598,7 +686,11 @@
   @GerritConfig(name = "auth.skipFullRefEvaluationIfAllRefsAreVisible", value = "false")
   public void advertisedReferencesOmitPrivateChangesOfOtherUsersWhenShortcutDisabled()
       throws Exception {
-    allow("refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> userTestRepository = cloneProject(project, user);
     try (Git git = userTestRepository.git()) {
@@ -612,8 +704,16 @@
 
   @Test
   public void advertisedReferencesOmitDraftCommentRefsOfOtherUsers() throws Exception {
-    allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
-    allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     DraftInput draftInput = new DraftInput();
@@ -632,8 +732,16 @@
 
   @Test
   public void advertisedReferencesOmitStarredChangesRefsOfOtherUsers() throws Exception {
-    allow(project, "refs/*", Permission.READ, REGISTERED_USERS);
-    allow(allUsersName, "refs/*", Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allUsersName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.accounts().self().starChange(cd3.getId().toString());
@@ -648,7 +756,10 @@
 
   @Test
   public void hideMetadata() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     // create change
     TestRepository<?> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userRef");
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index d783c7c..02f7b0a 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -17,12 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -47,10 +49,15 @@
 @NoHttpd
 public class SubmitOnPushIT extends AbstractDaemonTest {
   @Inject private ApprovalsUtil approvalsUtil;
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void submitOnPush() throws Exception {
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master%submit");
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
@@ -60,7 +67,11 @@
 
   @Test
   public void submitOnPushToRefsMetaConfig() throws Exception {
-    grant(project, "refs/for/refs/meta/config", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/meta/config").group(adminGroupUuid()))
+        .update();
 
     git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
     testRepo.reset(RefNames.REFS_CONFIG);
@@ -78,7 +89,11 @@
     push("refs/heads/master", "one change", "a.txt", "some content");
     testRepo.reset(objectId);
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "a.txt", "other content");
     r.assertErrorStatus();
@@ -94,7 +109,11 @@
     push(master, "one change", "a.txt", "some content");
     testRepo.reset(objectId);
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "b.txt", "other content");
     r.assertOkStatus();
@@ -107,7 +126,11 @@
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
 
-    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
     r =
         push(
             "refs/for/master%submit",
@@ -147,7 +170,11 @@
 
   @Test
   public void mergeOnPushToBranch() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
     r.assertOkStatus();
@@ -172,10 +199,14 @@
     enableCreateNewChangeForAllNotInTarget();
     String master = "refs/heads/master";
     String other = "refs/heads/other";
-    grant(project, master, Permission.PUSH);
-    grant(project, other, Permission.CREATE);
-    grant(project, other, Permission.PUSH);
-    RevCommit masterRev = getRemoteHead();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()))
+        .add(allow(Permission.CREATE).ref(other).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(other).group(adminGroupUuid()))
+        .update();
+    RevCommit masterRev = projectOperations.project(project).getHead("master");
     pushCommitTo(masterRev, other);
     PushOneCommit.Result r = createChange();
     r.assertOkStatus();
@@ -209,7 +240,11 @@
 
   @Test
   public void mergeOnPushToBranchWithNewPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     RevCommit c1 = r.getCommit();
@@ -243,7 +278,11 @@
 
   @Test
   public void mergeOnPushToBranchWithOldPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     RevCommit c1 = r.getCommit();
@@ -270,10 +309,14 @@
 
   @Test
   public void mergeMultipleOnPushToBranchWithNewPatchset() throws Exception {
-    grant(project, "refs/heads/master", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
 
     // Create 2 changes.
-    ObjectId initialHead = getRemoteHead();
+    ObjectId initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result r1 = createChange("Change 1", "a", "a");
     r1.assertOkStatus();
     PushOneCommit.Result r2 = createChange("Change 2", "b", "b");
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 2f551a5..afd41dc 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -22,9 +22,11 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -45,6 +47,8 @@
     return submitWholeTopicEnabledConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
   public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 9ebc3de..eef6e33 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -17,11 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -31,6 +33,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
 import java.util.ArrayDeque;
 import java.util.Map;
 import org.apache.commons.lang.RandomStringUtils;
@@ -70,6 +73,8 @@
     return submitByRebaseIfNecessaryConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
     allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
@@ -285,8 +290,12 @@
               .name(prefix + "sub" + i)
               .submitType(getSubmitType())
               .create();
-      grant(subKey[i], "refs/heads/*", Permission.PUSH);
-      grant(subKey[i], "refs/for/refs/heads/*", Permission.SUBMIT);
+      projectOperations
+          .project(subKey[i])
+          .forUpdate()
+          .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+          .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+          .update();
       sub[i] = cloneProject(subKey[i]);
     }
 
@@ -396,7 +405,7 @@
     gApi.changes().id(subChangeId).current().submit();
 
     expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
-    RevCommit superHead = getRemoteHead(superKey, "master");
+    RevCommit superHead = projectOperations.project(superKey).getHead("master");
     assertThat(superHead.getShortMessage()).contains("some message");
     assertThat(superHead.getId()).isNotEqualTo(superId);
   }
@@ -430,7 +439,7 @@
 
     gApi.changes().id(subChangeId).current().submit();
 
-    RevCommit superHead = getRemoteHead(superKey, "master");
+    RevCommit superHead = projectOperations.project(superKey).getHead("master");
     assertThat(superHead.getShortMessage()).isEqualTo("some message");
     assertThat(superHead.getId()).isEqualTo(superId);
   }
@@ -746,13 +755,13 @@
     approve(getChangeId(repoB, bDevHead).get());
 
     gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
-    assertThat(getRemoteHead(keyA, "refs/heads/master").getShortMessage())
+    assertThat(projectOperations.project(keyA).getHead("refs/heads/master").getShortMessage())
         .contains("some message in a master.txt");
-    assertThat(getRemoteHead(keyA, "refs/heads/dev").getShortMessage())
+    assertThat(projectOperations.project(keyA).getHead("refs/heads/dev").getShortMessage())
         .contains("some message in a dev.txt");
-    assertThat(getRemoteHead(keyB, "refs/heads/master").getShortMessage())
+    assertThat(projectOperations.project(keyB).getHead("refs/heads/master").getShortMessage())
         .contains("some message in b master.txt");
-    assertThat(getRemoteHead(keyB, "refs/heads/dev").getShortMessage())
+    assertThat(projectOperations.project(keyB).getHead("refs/heads/dev").getShortMessage())
         .contains("some message in b dev.txt");
   }
 
@@ -845,13 +854,13 @@
 
     sub1.git().fetch().call();
     RevWalk rw1 = sub1.getRevWalk();
-    RevCommit master1 = rw1.parseCommit(getRemoteHead(subKey1, "master"));
+    RevCommit master1 = rw1.parseCommit(projectOperations.project(subKey1).getHead("master"));
     RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
     assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();
 
     sub2.git().fetch().call();
     RevWalk rw2 = sub2.getRevWalk();
-    RevCommit master2 = rw2.parseCommit(getRemoteHead(subKey2, "master"));
+    RevCommit master2 = rw2.parseCommit(projectOperations.project(subKey2).getHead("master"));
     RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
     assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
 
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 26fe5e1..6991a250 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -37,7 +37,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_0);
+    return getConfig(ElasticVersion.V7_1);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index e7ce43f..be4fde0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
 import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
@@ -26,24 +29,30 @@
 import static com.google.gerrit.common.data.GlobalCapability.RUN_AS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
-import com.google.common.collect.Iterables;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class CapabilitiesIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void capabilitiesUser() throws Exception {
-    Iterable<String> all =
-        Iterables.filter(
-            GlobalCapability.getAllNames(),
-            c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c));
-
-    allowGlobalCapabilities(REGISTERED_USERS, all);
+    ImmutableList<String> all =
+        GlobalCapability.getAllNames().stream()
+            .filter(c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c))
+            .collect(toImmutableList());
+    TestProjectUpdate.Builder allowBuilder = projectOperations.allProjectsForUpdate();
+    all.forEach(c -> allowBuilder.add(allowCapability(c).group(REGISTERED_USERS)));
+    allowBuilder.update();
     try {
       RestResponse r = userRestSession.get("/accounts/self/capabilities");
       r.assertOK();
@@ -67,7 +76,9 @@
         }
       }
     } finally {
-      removeGlobalCapabilities(REGISTERED_USERS, all);
+      TestProjectUpdate.Builder removeBuilder = projectOperations.allProjectsForUpdate();
+      all.forEach(c -> removeBuilder.remove(capabilityKey(c).group(REGISTERED_USERS)));
+      removeBuilder.update();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 72ae859..ba3c7ea 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -18,6 +18,8 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
@@ -34,6 +36,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -92,6 +95,7 @@
   @Inject private ExternalIds externalIds;
   @Inject private ExternalIdReader externalIdReader;
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -121,7 +125,10 @@
 
   @Test
   public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     Collection<ExternalId> expectedIds = getAccountState(admin.id()).getExternalIds();
     List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
@@ -195,7 +202,10 @@
 
   @Test
   public void deleteExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
 
@@ -258,29 +268,33 @@
 
   @Test
   public void fetchExternalIdsBranch() throws Exception {
-    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
+    final TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
 
     // refs/meta/external-ids is only visible to users with the 'Access Database' capability
-    try {
-      fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
-      fail("expected TransportException");
-    } catch (TransportException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
-    }
+    TransportException thrown =
+        assertThrows(
+            TransportException.class, () -> fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
 
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     // re-clone to get new request context, otherwise the old global capabilities are still cached
     // in the IdentifiedUser object
-    allUsersRepo = cloneProject(allUsers, user);
-    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    TestRepository<InMemoryRepository> allUsersRepo2 = cloneProject(allUsers, user);
+    fetch(allUsersRepo2, RefNames.REFS_EXTERNAL_IDS);
   }
 
   @Test
   public void pushToExternalIdsBranch() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -305,7 +319,10 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -323,7 +340,10 @@
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
       throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -340,7 +360,10 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -357,7 +380,10 @@
 
   @Test
   public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -397,7 +423,10 @@
 
   private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
       throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
@@ -413,7 +442,10 @@
 
   @Test
   public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
@@ -434,7 +466,10 @@
 
   @Test
   public void checkConsistency() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.resetCurrentApiUser();
 
     insertValidExternalIds();
@@ -986,8 +1021,12 @@
   }
 
   private void allowPushOfExternalIds() {
-    grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.READ);
-    grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.PUSH);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_EXTERNAL_IDS).group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_EXTERNAL_IDS).group(adminGroupUuid()))
+        .update();
   }
 
   private void assertRefUpdateFailure(RemoteRefUpdate update, String msg) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index a27a6a9..1c342ee 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -15,6 +15,11 @@
 package com.google.gerrit.acceptance.rest.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -29,6 +34,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
@@ -60,7 +66,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import org.apache.http.Header;
@@ -74,6 +80,7 @@
   @Inject private ApprovalsUtil approvalsUtil;
   @Inject private ChangeMessagesUtil cmUtil;
   @Inject private CommentsUtil commentsUtil;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private RestSession anonRestSession;
@@ -166,7 +173,7 @@
   @Test
   public void voteOnBehalfOfLabelNotPermitted() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType verified = Util.verified();
+      LabelType verified = TestLabels.verified();
       u.getConfig().getLabelSections().put(verified.getName(), verified);
       u.save();
     }
@@ -567,53 +574,53 @@
   }
 
   private void allowCodeReviewOnBehalfOf() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType codeReviewType = Util.codeReview();
-      String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
-      String heads = "refs/heads/*";
-      AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), forCodeReviewAs, -1, 1, uuid, heads);
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .impersonation(true)
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
   }
 
   private void allowSubmitOnBehalfOf() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      String heads = "refs/heads/*";
-      AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(u.getConfig(), Permission.SUBMIT_AS, uuid, heads);
-      Util.allow(u.getConfig(), Permission.SUBMIT, uuid, heads);
-      LabelType codeReviewType = Util.codeReview();
-      Util.allow(u.getConfig(), Permission.forLabel(codeReviewType.getName()), -2, 2, uuid, heads);
-      u.save();
-    }
+    String heads = "refs/heads/*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT_AS).ref(heads).group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref(heads).group(REGISTERED_USERS))
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref(heads)
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
   }
 
   private void blockRead(GroupInfo group) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.READ, AccountGroup.uuid(group.id), "refs/heads/master");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(AccountGroup.uuid(group.id)))
+        .update();
   }
 
   private void allowRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.allow(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void removeRunAs() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      Util.remove(
-          u.getConfig(),
-          GlobalCapability.RUN_AS,
-          systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(capabilityKey(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
+        .update();
   }
 
   private static Header runAsHeader(Object user) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 02c44ef..cef599f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
@@ -22,10 +23,12 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
 import java.util.List;
 import java.util.Optional;
 import org.junit.Test;
@@ -80,10 +83,15 @@
           // Task deletion must be tested last
           RestCall.delete("/config/server/tasks/%s"));
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void configEndpoints() throws Exception {
     // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
+        .update();
 
     RestApiCallHelper.execute(adminRestSession, CONFIG_ENDPOINTS);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index c838cf9..12f32ad 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
 import static com.google.gerrit.server.restapi.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
 import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
@@ -216,7 +217,7 @@
         testRepo
             .commit()
             .message("A change")
-            .parent(getRemoteHead())
+            .parent(projectOperations.project(project).getHead("master"))
             .add(filename, "content")
             .insertChangeId()
             .create();
@@ -234,7 +235,11 @@
 
   private void createDefaultDashboard() throws Exception {
     String dashboardRef = REFS_DASHBOARDS + "team";
-    grant(project, "refs/meta/*", Permission.CREATE);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/meta/*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(project.get()).branch(dashboardRef).create(new BranchInput());
 
     try (Repository r = repoManager.openRepository(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index dfb8087..6ae6938 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -17,8 +17,12 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
@@ -41,6 +45,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
@@ -77,7 +82,6 @@
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.Submit;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -123,6 +127,7 @@
   @Inject private ApprovalsUtil approvalsUtil;
   @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   @Inject private IdentifiedUser.GenericFactory userFactory;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private Submit submitHandler;
 
@@ -161,16 +166,17 @@
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
   @Test
   public void submitSingleChange() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit).isEqualTo(initialHead);
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -189,12 +195,12 @@
 
   @Test
   public void submitMultipleChangesOtherMergeConflictPreview() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
@@ -260,11 +266,11 @@
           break;
         case CHERRY_PICK:
         default:
-          fail("Should not reach here.");
+          assert_().fail("Should not reach here.");
           break;
       }
 
-      RevCommit headAfterSubmit = getRemoteHead();
+      RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
       assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
       assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
       assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
@@ -273,7 +279,7 @@
 
   @Test
   public void submitMultipleChangesPreview() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
     PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
@@ -298,7 +304,7 @@
     }
 
     // check that the submit preview did not actually submit
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit).isEqualTo(initialHead);
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -313,7 +319,11 @@
   public void submitNoPermission() throws Throwable {
     // create project where submit is blocked
     Project.NameKey p = projectOperations.newProject().create();
-    block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS);
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -327,13 +337,13 @@
   public void noSelfSubmit() throws Throwable {
     // create project where submit is blocked for the change owner
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.block(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
+        .add(allow(Permission.SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -353,13 +363,13 @@
   public void onlySelfSubmit() throws Throwable {
     // create project where only the change owner can submit
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.block(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(CHANGE_OWNER))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
     PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -421,7 +431,7 @@
     Project.NameKey keyA = createProjectForPush(getSubmitType());
     TestRepository<?> repoA = cloneProject(keyA);
 
-    RevCommit initialHead = getRemoteHead(keyA, "master");
+    RevCommit initialHead = projectOperations.project(keyA).getHead("master");
 
     // Create the dev branch on the test project
     BranchInput in = new BranchInput();
@@ -552,7 +562,11 @@
     createBranch(BranchNameKey.create(project, "hidden"));
     PushOneCommit.Result hidden = createChange("refs/for/hidden/" + name("topic"));
     approve(hidden.getChangeId());
-    blockRead("refs/heads/hidden");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/hidden").group(REGISTERED_USERS))
+        .update();
 
     submit(
         visible.getChangeId(),
@@ -610,7 +624,7 @@
     // | /
     // I   -- master
     //
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit master = projectOperations.project(project).getHead("master");
     PushOneCommit stableTip =
         pushFactory.create(admin.newIdent(), testRepo, "Tip of branch stable", "stable.txt", "");
     PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
@@ -638,7 +652,7 @@
     // | /
     // I -- master
     //
-    RevCommit initial = getRemoteHead(project, "master");
+    RevCommit initial = projectOperations.project(project).getHead("master");
     // push directly to stable to S1
     PushOneCommit.Result s1 =
         pushFactory
@@ -675,7 +689,7 @@
     // create and submit a change
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the change back to NEW to simulate a failed submit that
     // merged the commit but failed to update the change status
@@ -684,7 +698,7 @@
     // submitting the change again should detect that the commit was already
     // merged and just fix the change status to be MERGED
     submit(change.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
@@ -698,7 +712,7 @@
     }
     submit(change2.getChangeId());
     assertMerged(change1.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the changes back to NEW to simulate a failed submit that
     // merged the commits but failed to update the change status
@@ -708,7 +722,7 @@
     // merged and just fix the change status to be MERGED
     submit(change1.getChangeId());
     submit(change2.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
@@ -722,7 +736,7 @@
     approve(change1.getChangeId());
     submit(change2.getChangeId());
     assertMerged(change1.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     // set the status of the second change back to NEW to simulate a failed
     // submit that merged the commits but failed to update the change status of
@@ -732,7 +746,7 @@
     // submitting the topic again should detect that the commits were already
     // merged and just fix the change status to be MERGED
     submit(change2.getChangeId());
-    assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterSubmit);
   }
 
   @Test
@@ -824,7 +838,7 @@
   public void submitWithCommitAndItsMergeCommitTogether() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create a stable branch and bootstrap it.
     gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
@@ -832,8 +846,8 @@
         pushFactory.create(user.newIdent(), testRepo, "initial commit", "a.txt", "a");
     PushOneCommit.Result change = push.to("refs/heads/stable");
 
-    RevCommit stable = getRemoteHead(project, "stable");
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit stable = projectOperations.project(project).getHead("stable");
+    RevCommit master = projectOperations.project(project).getHead("master");
 
     assertThat(master).isEqualTo(initialHead);
     assertThat(stable).isEqualTo(change.getCommit());
@@ -886,7 +900,7 @@
     assertMerged(mergeId);
     testRepo.git().fetch().call();
     RevWalk rw = testRepo.getRevWalk();
-    master = rw.parseCommit(getRemoteHead(project, "master"));
+    master = rw.parseCommit(projectOperations.project(project).getHead("master"));
     assertThat(rw.isMergedInto(merge, master)).isTrue();
     assertThat(rw.isMergedInto(fix, master)).isTrue();
   }
@@ -909,7 +923,7 @@
 
     testRepo.git().fetch().call();
     RevWalk rw = testRepo.getRevWalk();
-    RevCommit master = rw.parseCommit(getRemoteHead(project, "master"));
+    RevCommit master = rw.parseCommit(projectOperations.project(project).getHead("master"));
     RevCommit patchSet = parseCurrentRevision(rw, change.getChangeId());
     assertThat(rw.isMergedInto(patchSet, master)).isTrue();
 
@@ -952,13 +966,13 @@
 
     repoA.git().fetch().call();
     RevWalk rwA = repoA.getRevWalk();
-    RevCommit masterA = rwA.parseCommit(getRemoteHead(keyA, "master"));
+    RevCommit masterA = rwA.parseCommit(projectOperations.project(keyA).getHead("master"));
     RevCommit change1Ps = parseCurrentRevision(rwA, change1.getChangeId());
     assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
 
     repoB.git().fetch().call();
     RevWalk rwB = repoB.getRevWalk();
-    RevCommit masterB = rwB.parseCommit(getRemoteHead(keyB, "master"));
+    RevCommit masterB = rwB.parseCommit(projectOperations.project(keyB).getHead("master"));
     RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
     assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
 
@@ -973,7 +987,7 @@
     ci.matchAuthorToCommitterDate = InheritableBoolean.TRUE;
     gApi.projects().name(project.get()).config(ci);
 
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
 
@@ -987,7 +1001,7 @@
     }
 
     submit(change2.getChangeId());
-    assertAuthorAndCommitDateEquals(getRemoteHead());
+    assertAuthorAndCommitDateEquals(projectOperations.project(project).getHead("master"));
   }
 
   @Test
@@ -1077,7 +1091,8 @@
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
@@ -1097,7 +1112,8 @@
     assertThat(actual).hasSize(1);
 
     submit(change.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change.getCommit());
     assertTrees(project, actual);
   }
 
@@ -1267,7 +1283,7 @@
   protected void assertCherryPick(TestRepository<?> testRepo, boolean contentMerge)
       throws Throwable {
     assertRebase(testRepo, contentMerge);
-    RevCommit remoteHead = getRemoteHead();
+    RevCommit remoteHead = projectOperations.project(project).getHead("master");
     assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
     assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty();
   }
@@ -1275,7 +1291,7 @@
   protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge) throws Throwable {
     Repository repo = testRepo.getRepository();
     RevCommit localHead = getHead(repo, "HEAD");
-    RevCommit remoteHead = getRemoteHead();
+    RevCommit remoteHead = projectOperations.project(project).getHead("master");
     assertThat(localHead.getId()).isNotEqualTo(remoteHead.getId());
     assertThat(remoteHead.getParentCount()).isEqualTo(1);
     if (!contentMerge) {
@@ -1330,8 +1346,12 @@
   // TODO(hanwen): the submodule tests have a similar method; maybe we could share code?
   protected Project.NameKey createProjectForPush(SubmitType submitType) throws Throwable {
     Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
-    grant(project, "refs/heads/*", Permission.PUSH);
-    grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+        .update();
     return project;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index cad06fb..a4fa84b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -19,23 +19,26 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void submitWithMerge() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
@@ -49,11 +52,11 @@
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change3.getCommit());
@@ -62,11 +65,11 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge_Conflict() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -78,7 +81,7 @@
             + "Change could not be merged due to a path conflict. "
             + "Please rebase the change locally "
             + "and upload the rebased commit for review.");
-    assertThat(getRemoteHead()).isEqualTo(oldHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(oldHead);
   }
 
   @Test
@@ -88,7 +91,8 @@
     PushOneCommit.Result change2 = createChange();
     approve(change1.getChangeId());
     submit(change2.getChangeId());
-    assertThat(getRemoteHead().getId()).isEqualTo(change2.getCommit());
+    assertThat(projectOperations.project(project).getHead("master").getId())
+        .isEqualTo(change2.getCommit());
   }
 
   @Test
@@ -108,7 +112,7 @@
     approve(change1.getChangeId());
     submit(change2.getChangeId());
 
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getParents()).hasLength(2);
     assertThat(head.getParent(0)).isEqualTo(change1.getCommit());
     assertThat(head.getParent(1)).isEqualTo(change2.getCommit());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 18a9a24..e05e0b7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -17,12 +17,16 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -31,7 +35,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -40,6 +44,7 @@
 import org.junit.Test;
 
 public abstract class AbstractSubmitByRebase extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Override
@@ -54,18 +59,17 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithRebaseWithoutAddPatchSetPermission() throws Throwable {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(Util.codeReview().getName()),
-          -2,
-          2,
-          REGISTERED_USERS,
-          "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.ADD_PATCH_SET).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-2, 2))
+        .update();
 
     submitWithRebase(user);
   }
@@ -73,16 +77,16 @@
   protected ImmutableList<PushOneCommit.Result> submitWithRebase(TestAccount submitter)
       throws Throwable {
     requestScopeOperations.setApiUser(submitter.id());
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     assertRebase(testRepo, false);
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSecondSubmit.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertApproved(change2.getChangeId(), submitter);
     assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
@@ -103,10 +107,10 @@
 
   @Test
   public void submitWithRebaseMultipleChanges() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
       assertCurrentRevision(change1.getChangeId(), 2, headAfterFirstSubmit);
     } else {
@@ -127,7 +131,7 @@
     assertApproved(change3.getChangeId());
     assertApproved(change4.getChangeId());
 
-    RevCommit headAfterSecondSubmit = parse(getRemoteHead());
+    RevCommit headAfterSecondSubmit = parse(projectOperations.project(project).getHead("master"));
     assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4");
     assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit());
     assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit);
@@ -175,7 +179,7 @@
        |/
        * Initial empty repository
     */
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
 
     PushOneCommit change2Push =
@@ -193,7 +197,7 @@
     approve(change2.getChangeId());
     submit(change2.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParentCount()).isEqualTo(2);
 
     RevCommit headParent1 = parse(newHead.getParent(0).getId());
@@ -220,11 +224,11 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge_Conflict() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -232,7 +236,7 @@
         "Cannot rebase "
             + change2.getCommit().name()
             + ": The change could not be rebased due to a conflict during merge.");
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head).isEqualTo(headAfterFirstSubmit);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
@@ -252,7 +256,7 @@
 
   @Test
   public void submitAfterReorderOfCommits() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create two commits and push.
     RevCommit c1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
@@ -271,7 +275,7 @@
     approve(id1);
     approve(id2);
     submit(id1);
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
     assertChangeMergedEvents(id2, headAfterSubmit.name(), id1, headAfterSubmit.name());
@@ -279,7 +283,7 @@
 
   @Test
   public void submitChangesAfterBranchOnSecond() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange();
     approve(change.getChangeId());
@@ -293,7 +297,7 @@
     assertMerged(change2.getChangeId());
     assertMerged(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(this.project).getHead("master");
     assertRefUpdatedEvents(initialHead, newHead);
     assertChangeMergedEvents(
         change.getChangeId(), newHead.name(), change2.getChangeId(), newHead.name());
@@ -302,7 +306,7 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitFastForwardIdenticalTree() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a");
 
@@ -313,18 +317,18 @@
     testRepo.reset(initialHead);
     PushOneCommit.Result change0 = createChange("Change 0", "b.txt", "b");
     submit(change0.getChangeId());
-    RevCommit headAfterChange0 = getRemoteHead();
+    RevCommit headAfterChange0 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange0.getShortMessage()).isEqualTo("Change 0");
 
     submit(change1.getChangeId());
-    RevCommit headAfterChange1 = getRemoteHead();
+    RevCommit headAfterChange1 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange1.getShortMessage()).isEqualTo("Change 1");
     assertThat(headAfterChange0).isEqualTo(headAfterChange1.getParent(0));
 
     // Do manual rebase first.
     gApi.changes().id(change2.getChangeId()).current().rebase();
     submit(change2.getChangeId());
-    RevCommit headAfterChange2 = getRemoteHead();
+    RevCommit headAfterChange2 = projectOperations.project(project).getHead("master");
     assertThat(headAfterChange2.getShortMessage()).isEqualTo("Change 2");
     assertThat(headAfterChange1).isEqualTo(headAfterChange2.getParent(0));
 
@@ -351,7 +355,7 @@
     change1 =
         amendChange(change1.getChangeId(), "subject 1 amend", "fileName 2", "rework content 2");
     submit(change1.getChangeId());
-    headAfterChange1 = getRemoteHead();
+    headAfterChange1 = projectOperations.project(project).getHead("master");
 
     submitWithConflict(
         change2.getChangeId(),
@@ -359,13 +363,13 @@
             + change2.getCommit().getName()
             + ": "
             + "The change could not be rebased due to a conflict during merge.");
-    assertThat(getRemoteHead()).isEqualTo(headAfterChange1);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterChange1);
   }
 
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitChainOneByOneManualRebase() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", "content 1");
     PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", "content 2");
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index bdb710c..fec0d4b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -25,6 +25,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -45,6 +46,7 @@
 
 @NoHttpd
 public class AssigneeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @BeforeClass
@@ -131,20 +133,17 @@
   public void setAssigneeToInactiveUser() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.accounts().id(user.id().get()).setActive(false);
-    try {
-      setAssignee(r, user.email());
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Account '"
-                  + user.email()
-                  + "' only matches inactive accounts. To use an inactive account, retry with one"
-                  + " of the following exact account IDs:\n"
-                  + user.id()
-                  + ": User <user@example.com>");
-    }
+    UnresolvableAccountException thrown =
+        assertThrows(UnresolvableAccountException.class, () -> setAssignee(r, user.email()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Account '"
+                + user.email()
+                + "' only matches inactive accounts. To use an inactive account, retry with one"
+                + " of the following exact account IDs:\n"
+                + user.id()
+                + ": User <user@example.com>");
   }
 
   @Test
@@ -175,7 +174,11 @@
   @Test
   public void setAssigneeAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
-    grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_ASSIGNEE).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
index f05d4dc..def1a39 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
@@ -15,20 +15,25 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class ChangeIncludedInIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void includedInOpenChange() throws Exception {
     Result result = createChange();
@@ -49,7 +54,11 @@
         .containsExactly("master");
     assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags).isEmpty();
 
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
     gApi.projects().name(project.get()).tag("test-tag").create(new TagInput());
 
     assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 1e7fd38..8ddfa45 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -16,10 +16,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
 import static com.google.gerrit.server.restapi.change.DeleteChangeMessage.createNewChangeMessage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.util.RawParseUtils.decode;
@@ -29,6 +31,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.changes.DeleteChangeMessageInput;
@@ -57,6 +60,7 @@
 
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private String systemTimeZone;
@@ -160,17 +164,17 @@
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     requestScopeOperations.setApiUser(user.id());
 
-    try {
-      deleteOneChangeMessage(changeNum, 0, user, "spam");
-      fail("expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e.getMessage()).isEqualTo("administrate server not permitted");
-    }
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> deleteOneChangeMessage(changeNum, 0, user, "spam"));
+    assertThat(thrown).hasMessageThat().isEqualTo("administrate server not permitted");
   }
 
   @Test
   public void deleteCanBeAppliedWithAdministrateServerCapability() throws Exception {
-    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
     int changeNum = createOneChangeWithMultipleChangeMessagesInHistory();
     requestScopeOperations.setApiUser(user.id());
     deleteOneChangeMessage(changeNum, 0, user, "spam");
@@ -180,12 +184,15 @@
   public void deleteCannotBeAppliedWithEmptyChangeMessageUuid() throws Exception {
     String changeId = createChange().getChangeId();
 
-    try {
-      gApi.changes().id(changeId).message("").delete(new DeleteChangeMessageInput("spam"));
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("change message  not found");
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () ->
+                gApi.changes()
+                    .id(changeId)
+                    .message("")
+                    .delete(new DeleteChangeMessageInput("spam")));
+    assertThat(thrown).hasMessageThat().isEqualTo("change message  not found");
   }
 
   @Test
@@ -195,12 +202,11 @@
     String id = "8473b95934b5732ac55d26311a706c9c2bde9941";
     input.reason = "spam";
 
-    try {
-      gApi.changes().id(changeId).message(id).delete(input);
-      fail("expected ResourceNotFoundException");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo(String.format("change message %s not found", id));
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeId).message(id).delete(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(String.format("change message %s not found", id));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 70abe24..30d99ac 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -140,11 +143,24 @@
 
   private void grantApprove(Project.NameKey project, AccountGroup.UUID groupUUID, boolean exclusive)
       throws Exception {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", groupUUID, exclusive);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(groupUUID).range(-2, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/*"), exclusive)
+        .update();
   }
 
   private void blockApproveForChangeOwner(Project.NameKey project) throws Exception {
-    blockLabel("Code-Review", -2, 2, SystemGroupBackend.CHANGE_OWNER, "refs/heads/*", project);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            blockLabel("Code-Review")
+                .ref("refs/heads/*")
+                .group(SystemGroupBackend.CHANGE_OWNER)
+                .range(-2, 2))
+        .update();
   }
 
   private String createMyChange(TestRepository<InMemoryRepository> testRepo) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 76f6b98..e300c91 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
@@ -32,6 +33,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -67,6 +69,7 @@
 public class ChangeReviewersIT extends AbstractDaemonTest {
 
   @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -677,7 +680,11 @@
     // This test creates a new user so that it can explicitly check the REMOVE_REVIEWER permission
     // rather than bypassing the check because of project or ref ownership.
     TestAccount newUser = createAccounts(1, name("foo")).get(0);
-    grant(project, RefNames.REFS + "*", Permission.REMOVE_REVIEWER, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.REMOVE_REVIEWER).ref(RefNames.REFS + "*").group(REGISTERED_USERS))
+        .update();
 
     gApi.changes().id(r.getChangeId()).addReviewer(user.email());
     assertThatUserIsOnlyReviewer(r.getChangeId());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index a4ca7a3..57c0c8c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -15,13 +15,16 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -31,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -41,17 +43,18 @@
 import org.junit.Test;
 
 public class ConfigChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.OWNER, REGISTERED_USERS, "refs/*");
-      Util.allow(u.getConfig(), Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
-      Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, RefNames.REFS_CONFIG);
-      u.save();
-    }
-
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     fetchRefsMetaConfig();
   }
@@ -116,21 +119,18 @@
     String id = r.getChangeId();
 
     gApi.changes().id(id).current().review(ReviewInput.approve());
-    try {
-      gApi.changes().id(id).current().submit();
-      fail("expected submit to fail");
-    } catch (ResourceConflictException e) {
-      int n = gApi.changes().id(id).info()._number;
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(
-              "Failed to submit 1 change due to the following problems:\n"
-                  + "Change "
-                  + n
-                  + ": Change contains a project configuration that"
-                  + " changes the parent project.\n"
-                  + "The change must be submitted by a Gerrit administrator.");
-    }
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(id).current().submit());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            "Failed to submit 1 change due to the following problems:\n"
+                + "Change "
+                + gApi.changes().id(id).info()._number
+                + ": Change contains a project configuration that"
+                + " changes the parent project.\n"
+                + "The change must be submitted by a Gerrit administrator.");
 
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(allProjects.get());
     fetchRefsMetaConfig();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 2eb85d2..43cf655 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -28,6 +29,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -68,6 +70,7 @@
 import org.junit.Test;
 
 public class CreateChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @BeforeClass
@@ -260,7 +263,11 @@
   public void createChangeWithoutAccessToParentCommitFails() throws Exception {
     Map<String, PushOneCommit.Result> results =
         changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt");
-    block(project, "refs/heads/invisible-branch", READ, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/invisible-branch").group(REGISTERED_USERS))
+        .update();
 
     ChangeInput in = newChangeInput(ChangeStatus.NEW);
     in.branch = "visible-branch";
@@ -387,7 +394,7 @@
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    ObjectId remoteId = getRemoteHead();
+    ObjectId remoteId = projectOperations.project(project).getHead("master");
     assertThat(remoteId).isNotEqualTo(commitId);
 
     ChangeInput in = newMergeChangeInput("master", commitId.getName(), "");
@@ -451,7 +458,11 @@
   @Test
   public void createChangeOnExistingBranchNotPermitted() throws Exception {
     createBranch(BranchNameKey.create(project, "foo"));
-    blockRead("refs/heads/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.branch = "foo";
@@ -461,7 +472,11 @@
 
   @Test
   public void createChangeOnNonExistingBranchNotPermitted() throws Exception {
-    blockRead("refs/heads/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.branch = "foo";
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 0c5498b..542c6a9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.Objects.requireNonNull;
@@ -27,6 +28,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
@@ -41,6 +43,8 @@
 
 @NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @BeforeClass
   public static void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -267,7 +271,11 @@
   @Test
   public void addHashtagWithPermissionAllowed() throws Exception {
     PushOneCommit.Result r = createChange();
-    grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.EDIT_HASHTAGS).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     addHashtags(r, "MyHashtag");
     assertThatGet(r).containsExactly("MyHashtag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 0087268..e8fd295 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -27,7 +29,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -48,7 +49,11 @@
   @Test
   public void indexChangeOnNonVisibleBranch() throws Exception {
     String changeId = createChange().getChangeId();
-    blockRead("refs/heads/master");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     userRestSession.post("/changes/" + changeId + "/index/").assertNotFound();
   }
 
@@ -62,15 +67,15 @@
 
     // Create a project and restrict its visibility to the group
     Project.NameKey p = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(
-          u.getConfig(),
-          Permission.READ,
-          groupCache.get(AccountGroup.nameKey(group)).get().getGroupUUID(),
-          "refs/*");
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(
+            allow(Permission.READ)
+                .ref("refs/*")
+                .group(groupCache.get(AccountGroup.nameKey(group)).get().getGroupUUID()))
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Clone it and push a change as a regular user
     TestRepository<InMemoryRepository> repo = cloneProject(p, user);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 696e161..55cff17 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
@@ -24,6 +26,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
@@ -36,10 +39,8 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
@@ -49,6 +50,7 @@
 
 @NoHttpd
 public class MoveChangeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -181,10 +183,11 @@
     BranchNameKey newBranch =
         BranchNameKey.create(r.getChange().change().getProject(), "blocked_branch");
     createBranch(newBranch);
-    block(
-        "refs/for/" + newBranch.branch(),
-        Permission.PUSH,
-        systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.PUSH).ref("refs/for/" + newBranch.branch()).group(REGISTERED_USERS))
+        .update();
     AuthException thrown =
         assertThrows(AuthException.class, () -> move(r.getChangeId(), newBranch.branch()));
     assertThat(thrown).hasMessageThat().contains("move not permitted");
@@ -196,10 +199,14 @@
     PushOneCommit.Result r = createChange();
     BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
-    block(
-        r.getChange().change().getDest().branch(),
-        Permission.ABANDON,
-        systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            block(Permission.ABANDON)
+                .ref(r.getChange().change().getDest().branch())
+                .group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
         assertThrows(AuthException.class, () -> move(r.getChangeId(), newBranch.branch()));
@@ -236,20 +243,20 @@
     BranchNameKey newBranch = BranchNameKey.create(r.getChange().change().getProject(), "moveTest");
     createBranch(newBranch);
 
+    LabelType patchSetLock = TestLabels.patchSetLock();
     try (ProjectConfigUpdate u = updateProject(project)) {
-      LabelType patchSetLock = Util.patchSetLock();
       u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
-      AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(patchSetLock.getName()),
-          0,
-          1,
-          registeredUsers,
-          "refs/heads/*");
       u.save();
     }
-    grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(patchSetLock.getName())
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(0, 1))
+        .update();
     revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
 
     ResourceConflictException thrown =
@@ -275,16 +282,13 @@
     configLabel(testLabelB, LabelFunction.MAX_NO_BLOCK);
     configLabel(testLabelC, LabelFunction.NO_BLOCK);
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelB), -1, +1, registered, "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelC), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabelA).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .add(allowLabel(testLabelB).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .add(allowLabel(testLabelC).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     String changeId = createChange().getChangeId();
     gApi.changes().id(changeId).current().review(ReviewInput.reject());
@@ -324,12 +328,11 @@
     String testLabelA = "Label-A";
     configLabel(testLabelA, LabelFunction.MAX_WITH_BLOCK, Arrays.asList("refs/heads/master"));
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(
-          u.getConfig(), Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/master");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabelA).ref("refs/heads/master").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     String changeId = createChange().getChangeId();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index e0bca3a..a6fa9fc5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -131,7 +131,7 @@
   public void pushDraftsWithPrivateByDefaultAndDisablePrivateChangesTrue() throws Exception {
     setPrivateByDefault(project2, InheritableBoolean.TRUE);
 
-    RevCommit initialHead = getRemoteHead(project2, "master");
+    RevCommit initialHead = projectOperations.project(project2).getHead("master");
     TestRepository<InMemoryRepository> testRepo = cloneProject(project2);
     PushOneCommit.Result result =
         pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master%draft");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 7dbcbc1..16b7690 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -40,6 +41,7 @@
 
 public class SubmitByCherryPickIT extends AbstractSubmit {
   @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -48,11 +50,11 @@
 
   @Test
   public void submitWithCherryPickIfFastForwardPossible() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParent(0)).isEqualTo(change.getCommit().getParent(0));
 
     assertRefUpdatedEvents(initialHead, newHead);
@@ -61,16 +63,16 @@
 
   @Test
   public void submitWithCherryPick() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     assertThat(newHead.getParentCount()).isEqualTo(1);
     assertThat(newHead.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
@@ -108,19 +110,19 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
 
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, true);
-    RevCommit headAfterThirdSubmit = getRemoteHead();
+    RevCommit headAfterThirdSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterThirdSubmit.getParent(0)).isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
@@ -146,11 +148,11 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge_Conflict() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
     submitWithConflict(
@@ -162,7 +164,7 @@
             + "merged due to a path conflict. Please rebase the change locally and "
             + "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(newHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
     assertNoSubmitter(change2.getChangeId(), 1);
 
@@ -172,17 +174,17 @@
 
   @Test
   public void submitOutOfOrder() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "different content");
     submit(change3.getChangeId());
     assertCherryPick(testRepo, false);
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSecondSubmit.getParent(0)).isEqualTo(headAfterFirstSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterSecondSubmit);
@@ -200,11 +202,11 @@
 
   @Test
   public void submitOutOfOrder_Conflict() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 = createChange("Change 3", "b.txt", "different content");
@@ -217,7 +219,7 @@
             + "merged due to a path conflict. Please rebase the change locally and "
             + "upload the rebased commit for review.");
 
-    assertThat(getRemoteHead()).isEqualTo(newHead);
+    assertThat(projectOperations.project(project).getHead("master")).isEqualTo(newHead);
     assertCurrentRevision(change3.getChangeId(), 1, change3.getCommit());
     assertNoSubmitter(change3.getChangeId(), 1);
 
@@ -227,7 +229,7 @@
 
   @Test
   public void submitMultipleChanges() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -255,7 +257,7 @@
 
   @Test
   public void submitDependentNonConflictingChangesOutOfOrder() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -264,11 +266,11 @@
 
     // Submit succeeds; change2 is successfully cherry-picked onto head.
     submit(change2.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     // Submit succeeds; change is successfully cherry-picked onto head
     // (which was change2's cherry-pick).
     submit(change.getChangeId());
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
 
     // change is the new tip.
     List<RevCommit> log = getRemoteLog();
@@ -291,7 +293,7 @@
 
   @Test
   public void submitDependentConflictingChangesOutOfOrder() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b1");
@@ -323,7 +325,7 @@
 
   @Test
   public void submitSubsetOfDependentChanges() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -334,7 +336,7 @@
     // related to change 3 by topic or ancestor (due to cherrypicking!)
     approve(change2.getChangeId());
     submit(change3.getChangeId());
-    RevCommit newHead = getRemoteHead();
+    RevCommit newHead = projectOperations.project(project).getHead("master");
 
     assertNew(change.getChangeId());
     assertNew(change2.getChangeId());
@@ -346,7 +348,7 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitIdenticalTree() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a");
 
@@ -354,12 +356,13 @@
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a");
 
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterFirstSubmit.getShortMessage()).isEqualTo("Change 1");
 
     submit(change2.getChangeId(), new SubmitInput(), null, null);
 
-    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
+    assertThat(projectOperations.project(project).getHead("master"))
+        .isEqualTo(headAfterFirstSubmit);
 
     ChangeInfo info2 = get(change2.getChangeId(), MESSAGES);
     assertThat(info2.status).isEqualTo(ChangeStatus.MERGED);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index bfc4ae3..aff0cc2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -16,19 +16,23 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.inject.Inject;
 import java.util.Map;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
 public class SubmitByFastForwardIT extends AbstractSubmit {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -37,10 +41,10 @@
 
   @Test
   public void submitWithFastForward() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
@@ -51,7 +55,7 @@
 
   @Test
   public void submitMultipleChangesWithFastForward() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change = createChange();
     PushOneCommit.Result change2 = createChange();
@@ -64,7 +68,7 @@
     approve(id2);
     submit(id3);
 
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change3.getCommit());
     assertThat(updatedHead.getParent(0).getId()).isEqualTo(change2.getCommit());
     assertSubmitter(change.getChangeId(), 1);
@@ -83,7 +87,7 @@
 
   @Test
   public void submitTwoChangesWithFastForward_missingDependency() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 = createChange();
     PushOneCommit.Result change2 = createChange();
 
@@ -95,7 +99,7 @@
             + id1
             + ": needs Code-Review");
 
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(initialHead.getId());
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
@@ -103,11 +107,11 @@
 
   @Test
   public void submitFastForwardNotPossible_Conflict() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(initialHead);
     PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
 
@@ -128,7 +132,8 @@
             + ": Project policy requires "
             + "all submissions to be a fast-forward. Please rebase the change "
             + "locally and upload again for review.");
-    assertThat(getRemoteHead()).isEqualTo(headAfterFirstSubmit);
+    assertThat(projectOperations.project(project).getHead("master"))
+        .isEqualTo(headAfterFirstSubmit);
     assertSubmitter(change.getChangeId(), 1);
 
     assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
@@ -137,10 +142,14 @@
 
   @Test
   public void submitSameCommitsAsInExperimentalBranch() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
-    grant(project, "refs/heads/*", Permission.CREATE);
-    grant(project, "refs/heads/experimental", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.PUSH).ref("refs/heads/experimental").group(adminGroupUuid()))
+        .update();
 
     RevCommit c1 = commitBuilder().add("b.txt", "1").message("commit at tip").create();
     String id1 = GitUtil.getChangeId(testRepo, c1).get();
@@ -153,9 +162,9 @@
         .isEqualTo(c1.getId());
 
     submit(id1);
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
 
-    assertThat(getRemoteHead().getId()).isEqualTo(c1.getId());
+    assertThat(projectOperations.project(project).getHead("master").getId()).isEqualTo(c1.getId());
     assertSubmitter(id1, 1);
 
     assertRefUpdatedEvents(initialHead, headAfterSubmit);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 3b835a2..f80bdca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -17,11 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -30,10 +33,10 @@
 
   @Test
   public void submitWithMergeIfFastForwardPossible() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit headAfterSubmit = getRemoteHead();
+    RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterSubmit.getParentCount()).isEqualTo(2);
     assertThat(headAfterSubmit.getParent(0)).isEqualTo(initialHead);
     assertThat(headAfterSubmit.getParent(1)).isEqualTo(change.getCommit());
@@ -47,7 +50,7 @@
 
   @Test
   public void submitMultipleChanges() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Submit a change so that the remote head advances
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index ab9eed4..5c28968 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -16,6 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -65,10 +68,10 @@
 
   @Test
   public void submitWithFastForward() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit updatedHead = getRemoteHead();
+    RevCommit updatedHead = projectOperations.project(project).getHead("master");
     assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
     assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
     assertSubmitter(change.getChangeId(), 1);
@@ -81,7 +84,7 @@
 
   @Test
   public void submitMultipleChanges() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     testRepo.reset(initialHead);
     PushOneCommit.Result change = createChange("Change 1", "b", "b");
@@ -142,8 +145,8 @@
     Project.NameKey p2 = projectOperations.newProject().create();
     Project.NameKey p3 = projectOperations.newProject().create();
 
-    RevCommit initialHead2 = getRemoteHead(p2, "master");
-    RevCommit initialHead3 = getRemoteHead(p3, "master");
+    RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
+    RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -223,9 +226,9 @@
     TestRepository<?> repo2 = cloneProject(p2);
     TestRepository<?> repo3 = cloneProject(p3);
 
-    RevCommit initialHead1 = getRemoteHead(p1, "master");
-    RevCommit initialHead2 = getRemoteHead(p2, "master");
-    RevCommit initialHead3 = getRemoteHead(p3, "master");
+    RevCommit initialHead1 = projectOperations.project(p1).getHead("master");
+    RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
+    RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
 
     PushOneCommit.Result change1a =
         createChange(
@@ -312,12 +315,12 @@
 
   @Test
   public void submitWithMergedAncestorsOnOtherBranch() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     PushOneCommit.Result change1 =
         createChange(testRepo, "master", "base commit", "a.txt", "1", "");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
 
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
@@ -361,11 +364,11 @@
 
   @Test
   public void submitWithOpenAncestorsOnOtherBranch() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change1 =
         createChange(testRepo, "master", "base commit", "a.txt", "1", "");
     submit(change1.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
 
     gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
 
@@ -393,7 +396,7 @@
 
     Project.NameKey p3 = projectOperations.newProject().create();
     TestRepository<?> repo3 = cloneProject(p3);
-    RevCommit repo3Head = getRemoteHead(p3, "master");
+    RevCommit repo3Head = projectOperations.project(p3).getHead("master");
     PushOneCommit.Result change3b =
         createChange(
             repo3,
@@ -434,7 +437,7 @@
 
   @Test
   public void gerritWorkflow() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // We'll setup a master and a stable branch.
     // Then we create a change to be applied to master, which is
@@ -460,8 +463,8 @@
     gApi.changes().id(cherryId).current().submit();
 
     // Create the merge locally
-    RevCommit stable = getRemoteHead(project, "stable");
-    RevCommit master = getRemoteHead(project, "master");
+    RevCommit stable = projectOperations.project(project).getHead("stable");
+    RevCommit master = projectOperations.project(project).getHead("master");
     testRepo.git().fetch().call();
     testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
     testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
@@ -607,8 +610,12 @@
 
   @Test
   public void dependencyOnChangeForNonVisibleBranchPreventsMerge() throws Throwable {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", REGISTERED_USERS, false);
-    grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Create a change
     PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
@@ -628,17 +635,20 @@
         .branch(secretBranch.branch())
         .create(new BranchInput());
     gApi.changes().id(changeResult.getChangeId()).move(secretBranch.branch());
-    block(secretBranch.branch(), "read", ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block("read").ref(secretBranch.branch()).group(ANONYMOUS_USERS))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
-    try {
-      gApi.changes().id(changeResult.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + changeResult.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeResult.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
 
     // Submit is expected to fail.
     submitWithConflict(
@@ -662,8 +672,12 @@
 
   @Test
   public void dependencyOnHiddenChangePreventsMerge() throws Throwable {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", REGISTERED_USERS, false);
-    grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     // Create a change
     PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
@@ -682,24 +696,23 @@
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see the first change.
-    try {
-      gApi.changes().id(changeResult.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + changeResult.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class,
+            () -> gApi.changes().id(changeResult.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
 
     // Submit is expected to fail.
-    try {
-      gApi.changes().id(change2Result.getChangeId()).current().submit();
-      fail("expected failure");
-    } catch (AuthException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "A change to be submitted with "
-                  + change2Result.getChange().getId().get()
-                  + " is not visible");
-    }
+    AuthException thrown2 =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(change2Result.getChangeId()).current().submit());
+    assertThat(thrown2)
+        .hasMessageThat()
+        .isEqualTo(
+            "A change to be submitted with "
+                + change2Result.getChange().getId().get()
+                + " is not visible");
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
@@ -716,10 +729,18 @@
     Project.NameKey p1 = projectOperations.newProject().create();
     Project.NameKey p2 = projectOperations.newProject().create();
 
-    grantLabel("Code-Review", -2, 2, p1, "refs/heads/*", REGISTERED_USERS, false);
-    grant(p1, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
-    grantLabel("Code-Review", -2, 2, p2, "refs/heads/*", REGISTERED_USERS, false);
-    grant(p2, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
+    projectOperations
+        .project(p1)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(p2)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -742,24 +763,21 @@
     requestScopeOperations.setApiUser(user.id());
 
     // Verify that user cannot see change2a
-    try {
-      gApi.changes().id(change2a.getChangeId()).get();
-      fail("expected failure");
-    } catch (ResourceNotFoundException e) {
-      assertThat(e.getMessage()).isEqualTo("Not found: " + change2a.getChangeId());
-    }
+    ResourceNotFoundException thrown =
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.changes().id(change2a.getChangeId()).get());
+    assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + change2a.getChangeId());
 
     // Submit is expected to fail.
-    try {
-      gApi.changes().id(change1.getChangeId()).current().submit();
-      fail("expected failure");
-    } catch (AuthException e) {
-      assertThat(e.getMessage())
-          .isEqualTo(
-              "A change to be submitted with "
-                  + change1.getChange().getId().get()
-                  + " is not visible");
-    }
+    AuthException thrown2 =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(change1.getChangeId()).current().submit());
+    assertThat(thrown2)
+        .hasMessageThat()
+        .isEqualTo(
+            "A change to be submitted with "
+                + change1.getChange().getId().get()
+                + " is not visible");
     assertRefUpdatedEvents();
     assertChangeMergedEvents();
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index 5d5887d..1808480 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -44,6 +45,7 @@
 public class SubmitByRebaseAlwaysIT extends AbstractSubmitByRebase {
   @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers;
   @Inject private DynamicItem<UrlFormatter> urlFormatter;
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -53,11 +55,11 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithPossibleFastForward() throws Throwable {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
 
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getId()).isNotEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
@@ -127,14 +129,11 @@
         };
     ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
     try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
-      try {
-        submitWithRebase();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        Throwable cause = Throwables.getRootCause(e);
-        assertThat(cause).isInstanceOf(RuntimeException.class);
-        assertThat(cause).hasMessageThat().isEqualTo("boom");
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> submitWithRebase());
+      Throwable cause = Throwables.getRootCause(thrown);
+      assertThat(cause).isInstanceOf(RuntimeException.class);
+      assertThat(cause).hasMessageThat().isEqualTo("boom");
     }
   }
 
@@ -143,19 +142,16 @@
     ChangeMessageModifier modifier1 = (msg, orig, tip, dest) -> null;
     ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
     try (AutoCloseable ignored = installChangeMessageModifiers(modifier1, modifier2)) {
-      try {
-        submitWithRebase();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        Throwable cause = Throwables.getRootCause(e);
-        assertThat(cause).isInstanceOf(RuntimeException.class);
-        assertThat(cause)
-            .hasMessageThat()
-            .isEqualTo(
-                modifier1.getClass().getName()
-                    + ".onSubmit from plugin modifier-1 returned null instead of new commit"
-                    + " message");
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> submitWithRebase());
+      Throwable cause = Throwables.getRootCause(thrown);
+      assertThat(cause).isInstanceOf(RuntimeException.class);
+      assertThat(cause)
+          .hasMessageThat()
+          .isEqualTo(
+              modifier1.getClass().getName()
+                  + ".onSubmit from plugin modifier-1 returned null instead of new commit"
+                  + " message");
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index 1b71a2f..01b58ee 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -18,12 +18,15 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.inject.Inject;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 public class SubmitByRebaseIfNecessaryIT extends AbstractSubmitByRebase {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -33,10 +36,10 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithFastForward() throws Throwable {
-    RevCommit oldHead = getRemoteHead();
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    RevCommit head = getRemoteHead();
+    RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head.getId()).isEqualTo(change.getCommit());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
@@ -51,19 +54,19 @@
   @Test
   @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Throwable {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change = createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
-    RevCommit headAfterFirstSubmit = getRemoteHead();
+    RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
     PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
-    RevCommit headAfterSecondSubmit = getRemoteHead();
+    RevCommit headAfterSecondSubmit = projectOperations.project(project).getHead("master");
     testRepo.reset(change.getCommit());
     PushOneCommit.Result change3 = createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     assertRebase(testRepo, true);
-    RevCommit headAfterThirdSubmit = getRemoteHead();
+    RevCommit headAfterThirdSubmit = projectOperations.project(project).getHead("master");
     assertThat(headAfterThirdSubmit.getParent(0)).isEqualTo(headAfterSecondSubmit);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, headAfterThirdSubmit);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 9bbe1dd..c1e0e9e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -16,6 +16,9 @@
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static java.util.stream.Collectors.toList;
 
@@ -160,8 +163,12 @@
     List<SuggestedReviewerInfo> reviewers;
 
     requestScopeOperations.setApiUser(user3.id());
-    block("refs/*", "read", ANONYMOUS_USERS);
-    allow("refs/*", "read", group1);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block("read").ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow("read").ref("refs/*").group(group1))
+        .update();
     reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).isEmpty();
   }
@@ -178,7 +185,10 @@
 
     // Clear cached group info.
     requestScopeOperations.setApiUser(user1.id());
-    allowGlobalCapabilities(group1, GlobalCapability.VIEW_ALL_ACCOUNTS);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.VIEW_ALL_ACCOUNTS).group(group1))
+        .update();
     reviewers = suggestReviewers(changeId, user2.username(), 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(user2.fullName());
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index 7ef915b..daeb032 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -15,19 +15,24 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH;
 import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH_ALL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.restapi.config.PostCaches;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import org.junit.Test;
 
 public class CacheOperationsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void flushAll() throws Exception {
@@ -124,8 +129,11 @@
 
   @Test
   public void flushWebSessions_Forbidden() throws Exception {
-    allowGlobalCapabilities(
-        REGISTERED_USERS, GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+        .add(allowCapability(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+        .update();
     try {
       RestResponse r =
           userRestSession.post(
@@ -138,8 +146,11 @@
               "/config/server/caches/", new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")))
           .assertForbidden();
     } finally {
-      removeGlobalCapabilities(
-          REGISTERED_USERS, GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(capabilityKey(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+          .remove(capabilityKey(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+          .update();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index caecefa..a161ec4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -15,15 +15,20 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 public class FlushCacheIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void flushCache() throws Exception {
@@ -65,8 +70,11 @@
 
   @Test
   public void flushWebSessionsCache_Forbidden() throws Exception {
-    allowGlobalCapabilities(
-        REGISTERED_USERS, GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+        .add(allowCapability(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+        .update();
     try {
       RestResponse r = userRestSession.post("/config/server/caches/accounts/flush");
       r.assertOK();
@@ -74,8 +82,11 @@
 
       userRestSession.post("/config/server/caches/web_sessions/flush").assertForbidden();
     } finally {
-      removeGlobalCapabilities(
-          REGISTERED_USERS, GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(capabilityKey(GlobalCapability.FLUSH_CACHES).group(REGISTERED_USERS))
+          .remove(capabilityKey(GlobalCapability.VIEW_CACHES).group(REGISTERED_USERS))
+          .update();
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index bea1748..e2818d2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -21,14 +21,18 @@
 import static com.google.gerrit.acceptance.GitUtil.updateAnnotatedTag;
 import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.ANNOTATED;
 import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.LIGHTWEIGHT;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
@@ -50,6 +54,8 @@
     }
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   private RevCommit initialHead;
   private TagType tagType;
 
@@ -58,7 +64,7 @@
     // clone with user to avoid inherited tag permissions of admin user
     testRepo = cloneProject(project, user);
 
-    initialHead = getRemoteHead();
+    initialHead = projectOperations.project(project).getHead("master");
     tagType = getTagType();
   }
 
@@ -207,7 +213,11 @@
     }
 
     if (!newCommit) {
-      grant(project, "refs/for/refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS);
+      projectOperations
+          .project(project)
+          .forUpdate()
+          .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(REGISTERED_USERS))
+          .update();
       pushHead(testRepo, "refs/for/master%submit");
     }
 
@@ -229,26 +239,46 @@
   }
 
   private void allowTagCreation() throws Exception {
-    grant(project, "refs/tags/*", tagType.createPermission, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(tagType.createPermission).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowPushOnRefsTags() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.PUSH, false, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private void allowForcePushOnRefsTags() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.PUSH, true, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(REGISTERED_USERS).force(true))
+        .update();
   }
 
   private void allowTagDeletion() throws Exception {
     removePushFromRefsTags();
-    grant(project, "refs/tags/*", Permission.DELETE, true, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/tags/*").group(REGISTERED_USERS).force(true))
+        .update();
   }
 
   private void removePushFromRefsTags() throws Exception {
-    removePermission(project, "refs/tags/*", Permission.PUSH);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(permissionKey(Permission.PUSH).ref("refs/tags/*"))
+        .update();
   }
 
   private void commit(PersonIdent ident, String subject) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 503ebcc..bb043c2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -15,6 +15,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -34,7 +35,6 @@
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -127,7 +127,7 @@
 
   @Test
   public void addAccessSection() throws Exception {
-    RevCommit initialHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
+    RevCommit initialHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
 
     ProjectAccessInput accessInput = newProjectAccessInput();
     AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
@@ -137,7 +137,7 @@
 
     assertThat(pApi().access().local).isEqualTo(accessInput.add);
 
-    RevCommit updatedHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
+    RevCommit updatedHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
     eventRecorder.assertRefUpdatedEvents(
         newProjectName.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
   }
@@ -170,7 +170,11 @@
 
   @Test
   public void createAccessChange() throws Exception {
-    allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
+    projectOperations
+        .project(newProjectName)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
     // User can see the branch
     requestScopeOperations.setApiUser(user.id());
     pApi().branch("refs/heads/master").get();
@@ -207,12 +211,7 @@
 
     // check that the change took effect.
     requestScopeOperations.setApiUser(user.id());
-    try {
-      BranchInfo info = pApi().branch("refs/heads/master").get();
-      fail("wanted failure, got " + newGson().toJson(info));
-    } catch (ResourceNotFoundException e) {
-      // OK.
-    }
+    assertThrows(ResourceNotFoundException.class, () -> pApi().branch("refs/heads/master").get());
 
     // Restore.
     accessInput.add.clear();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
index 10e3e99..22fb829 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CheckMergeabilityIT.java
@@ -20,12 +20,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.reviewdb.client.BranchNameKey;
+import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.RefSpec;
@@ -34,6 +36,8 @@
 
 public class CheckMergeabilityIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   private BranchNameKey branch;
 
   @Before
@@ -44,7 +48,7 @@
 
   @Test
   public void checkMergeableCommit() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -79,7 +83,7 @@
 
   @Test
   public void checkUnMergeableCommit() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -114,7 +118,7 @@
 
   @Test
   public void checkOursMergeStrategy() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
@@ -208,7 +212,7 @@
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    ObjectId remoteId = getRemoteHead();
+    ObjectId remoteId = projectOperations.project(project).getHead("master");
     assertThat(remoteId).isNotEqualTo(commitId);
     assertContentMerged("master", commitId.getName(), "recursive");
   }
@@ -234,7 +238,7 @@
 
   @Test
   public void checkInvalidStrategy() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     testRepo
         .branch("HEAD")
         .commit()
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 96ad91c..41fa128 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -23,6 +25,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
@@ -40,6 +43,7 @@
 import org.junit.Test;
 
 public class CreateBranchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private BranchNameKey testBranch;
@@ -103,15 +107,23 @@
   @Test
   public void createMetaBranch() throws Exception {
     String metaRef = RefNames.REFS_META + "foo";
-    allow(metaRef, Permission.CREATE, REGISTERED_USERS);
-    allow(metaRef, Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(metaRef).group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(metaRef).group(REGISTERED_USERS))
+        .update();
     assertCreateSucceeds(BranchNameKey.create(project, metaRef));
   }
 
   @Test
   public void createUserBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .update();
     assertCreateFails(
         BranchNameKey.create(allUsers, RefNames.refsUsers(Account.id(1))),
         RefNames.refsUsers(admin.id()),
@@ -121,8 +133,12 @@
 
   @Test
   public void createGroupBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
     assertCreateFails(
         BranchNameKey.create(allUsers, RefNames.refsGroups(AccountGroup.uuid("foo"))),
         RefNames.refsGroups(adminGroupUuid()),
@@ -131,11 +147,19 @@
   }
 
   private void blockCreateReference() throws Exception {
-    block("refs/*", Permission.CREATE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private BranchApi branch(BranchNameKey branch) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 894d79f..0b9c3a9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -19,6 +19,8 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -31,6 +33,7 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
@@ -73,6 +76,7 @@
 import org.junit.Test;
 
 public class CreateProjectIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -324,7 +328,12 @@
 
   @Test
   public void createProjectWithCapability() throws Exception {
-    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     try {
       requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
@@ -332,8 +341,12 @@
       ProjectInfo p = gApi.projects().create(in).get();
       assertThat(p.name).isEqualTo(in.name);
     } finally {
-      removeGlobalCapabilities(
-          SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.CREATE_PROJECT)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
   }
 
@@ -356,7 +369,12 @@
   public void createProjectWithCreateProjectCapabilityAndParentNotVisible() throws Exception {
     Project parent = projectCache.get(allProjects).getProject();
     parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
-    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.CREATE_PROJECT)
+                .group(SystemGroupBackend.REGISTERED_USERS))
+        .update();
     try {
       requestScopeOperations.setApiUser(user.id());
       ProjectInput in = new ProjectInput();
@@ -365,8 +383,12 @@
       assertThat(p.name).isEqualTo(in.name);
     } finally {
       parent.setState(com.google.gerrit.extensions.client.ProjectState.ACTIVE);
-      removeGlobalCapabilities(
-          SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+      projectOperations
+          .allProjectsForUpdate()
+          .remove(
+              capabilityKey(GlobalCapability.CREATE_PROJECT)
+                  .group(SystemGroupBackend.REGISTERED_USERS))
+          .update();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index f95342a..c44c11a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -23,7 +25,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
@@ -123,8 +124,12 @@
   @Test
   public void deleteMetaBranch() throws Exception {
     String metaRef = RefNames.REFS_META + "foo";
-    allow(metaRef, Permission.CREATE, REGISTERED_USERS);
-    allow(metaRef, Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(metaRef).group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(metaRef).group(REGISTERED_USERS))
+        .update();
 
     BranchNameKey metaBranch = BranchNameKey.create(project, metaRef);
     branch(metaBranch).create(new BranchInput());
@@ -138,14 +143,8 @@
     projectOperations
         .project(allUsers)
         .forUpdate()
-        .add(
-            TestProjectUpdate.allow(Permission.CREATE)
-                .ref(RefNames.REFS_USERS + "*")
-                .group(REGISTERED_USERS))
-        .add(
-            TestProjectUpdate.allow(Permission.PUSH)
-                .ref(RefNames.REFS_USERS + "*")
-                .group(REGISTERED_USERS))
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_USERS + "*").group(REGISTERED_USERS))
         .update();
 
     ResourceConflictException thrown =
@@ -157,8 +156,12 @@
 
   @Test
   public void deleteGroupBranch_Conflict() throws Exception {
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
-    allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(allUsers)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
+        .update();
 
     ResourceConflictException thrown =
         assertThrows(
@@ -173,24 +176,32 @@
     projectOperations
         .project(project)
         .forUpdate()
-        .add(
-            TestProjectUpdate.block(Permission.PUSH)
-                .ref("refs/heads/*")
-                .group(ANONYMOUS_USERS)
-                .force(true))
+        .add(block(Permission.PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
         .update();
   }
 
   private void grantForcePush() throws Exception {
-    grant(project, "refs/heads/*", Permission.PUSH, true, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantDelete() throws Exception {
-    allow("refs/*", Permission.DELETE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private BranchApi branch(BranchNameKey branch) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index f640c7c..523b711 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
@@ -25,6 +26,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -47,12 +49,17 @@
   private static final ImmutableList<String> BRANCHES =
       ImmutableList.of("refs/heads/test-1", "refs/heads/test-2", "test-3", "refs/meta/foo");
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
-    allow("refs/*", Permission.CREATE, REGISTERED_USERS);
-    allow("refs/*", Permission.PUSH, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     for (String name : BRANCHES) {
       project().branch(name).create(new BranchInput());
     }
@@ -76,12 +83,8 @@
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = branchToDelete;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteBranches(input);
-      fail("Expected AuthException");
-    } catch (AuthException e) {
-      assertThat(e).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
-    }
+    AuthException thrown = assertThrows(AuthException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
     requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
@@ -91,12 +94,9 @@
     DeleteBranchesInput input = new DeleteBranchesInput();
     input.branches = BRANCHES;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(errorMessageForBranches(BRANCHES));
     requestScopeOperations.setApiUser(admin.id());
     assertBranches(BRANCHES);
   }
@@ -107,14 +107,11 @@
     List<String> branches = Lists.newArrayList(BRANCHES);
     branches.add("refs/heads/does-not-exist");
     input.branches = branches;
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
     assertBranchesDeleted(BRANCHES);
   }
 
@@ -126,14 +123,11 @@
     List<String> branches = Lists.newArrayList("refs/heads/does-not-exist");
     branches.addAll(BRANCHES);
     input.branches = branches;
-    try {
-      project().deleteBranches(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteBranches(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForBranches(ImmutableList.of("refs/heads/does-not-exist")));
     assertBranchesDeleted(BRANCHES);
   }
 
@@ -177,7 +171,7 @@
   private HashMap<String, RevCommit> initialRevisions(List<String> branches) throws Exception {
     HashMap<String, RevCommit> result = new HashMap<>();
     for (String branch : branches) {
-      result.put(branch, getRemoteHead(project, branch));
+      result.put(branch, projectOperations.project(project).getHead(branch));
     }
     return result;
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 892c375..9770031 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -22,7 +24,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagApi;
@@ -37,6 +39,7 @@
 public class DeleteTagIT extends AbstractDaemonTest {
   private static final String TAG = "refs/tags/test";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -102,24 +105,32 @@
     projectOperations
         .project(project)
         .forUpdate()
-        .add(
-            TestProjectUpdate.block(Permission.PUSH)
-                .ref("refs/tags/*")
-                .group(ANONYMOUS_USERS)
-                .force(true))
+        .add(block(Permission.PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS).force(true))
         .update();
   }
 
   private void grantForcePush() throws Exception {
-    grant(project, "refs/tags/*", Permission.PUSH, true, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS).force(true))
+        .update();
   }
 
   private void grantDelete() throws Exception {
-    allow("refs/tags/*", Permission.DELETE, ANONYMOUS_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.DELETE).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
   }
 
   private void grantOwner() throws Exception {
-    allow("refs/tags/*", Permission.OWNER, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/tags/*").group(REGISTERED_USERS))
+        .update();
   }
 
   private TagApi tag() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
index fae9d00..46e2345 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
@@ -23,6 +24,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
@@ -41,6 +43,7 @@
   private static final ImmutableList<String> TAGS =
       ImmutableList.of("refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3", "test-4");
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
@@ -66,12 +69,9 @@
     DeleteTagsInput input = new DeleteTagsInput();
     input.tags = TAGS;
     requestScopeOperations.setApiUser(user.id());
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown).hasMessageThat().isEqualTo(errorMessageForTags(TAGS));
     requestScopeOperations.setApiUser(admin.id());
     assertTags(TAGS);
   }
@@ -82,14 +82,11 @@
     List<String> tags = Lists.newArrayList(TAGS);
     tags.add("refs/tags/does-not-exist");
     input.tags = tags;
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
     assertTagsDeleted();
   }
 
@@ -101,14 +98,11 @@
     List<String> tags = Lists.newArrayList("refs/tags/does-not-exist");
     tags.addAll(TAGS);
     input.tags = tags;
-    try {
-      project().deleteTags(input);
-      fail("Expected ResourceConflictException");
-    } catch (ResourceConflictException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
-    }
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> project().deleteTags(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(errorMessageForTags(ImmutableList.of("refs/tags/does-not-exist")));
     assertTagsDeleted();
   }
 
@@ -128,7 +122,7 @@
     HashMap<String, RevCommit> result = new HashMap<>();
     for (String tag : tags) {
       String ref = prefixRef(tag);
-      result.put(ref, getRemoteHead(project, ref));
+      result.put(ref, projectOperations.project(project).getHead(ref));
     }
     return result;
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 18c706b..f9011c7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -15,14 +15,18 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -32,12 +36,18 @@
 import org.junit.Test;
 
 public class GetCommitIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   private TestRepository<Repository> repo;
 
   @Before
   public void setUp() throws Exception {
     repo = GitUtil.newTestRepository(repoManager.openRepository(project));
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
   }
 
   @After
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index d1364f0..a797b98 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -15,13 +15,17 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefs;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -32,6 +36,7 @@
 
 @NoHttpd
 public class ListBranchesIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -43,7 +48,11 @@
 
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThrows(
         ResourceNotFoundException.class,
@@ -62,7 +71,7 @@
   public void listBranches() throws Exception {
     String master = pushTo("refs/heads/master").getCommit().name();
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    String refsConfig = getRemoteHead(project, RefNames.REFS_CONFIG).name();
+    String refsConfig = projectOperations.project(project).getHead(RefNames.REFS_CONFIG).name();
     assertRefs(
         ImmutableList.of(
             branch("HEAD", "master", false),
@@ -74,7 +83,11 @@
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
-    blockRead("refs/heads/dev");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/dev").group(REGISTERED_USERS))
+        .update();
     String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
     requestScopeOperations.setApiUser(user.id());
@@ -87,7 +100,11 @@
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
-    blockRead("refs/heads/master");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
     pushTo("refs/heads/master");
     String dev = pushTo("refs/heads/dev").getCommit().name();
     requestScopeOperations.setApiUser(user.id());
@@ -99,7 +116,10 @@
   public void listBranchesUsingPagination() throws Exception {
     BranchInfo head = branch("HEAD", "master", false);
     BranchInfo refsConfig =
-        branch(RefNames.REFS_CONFIG, getRemoteHead(project, RefNames.REFS_CONFIG).name(), false);
+        branch(
+            RefNames.REFS_CONFIG,
+            projectOperations.project(project).getHead(RefNames.REFS_CONFIG).name(),
+            false);
     BranchInfo master =
         branch("refs/heads/master", pushTo("refs/heads/master").getCommit().getName(), false);
     BranchInfo branch1 =
@@ -173,11 +193,6 @@
   }
 
   private void assertBadRequest(ListRefsRequest<BranchInfo> req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index f29069c..1443c99 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -16,7 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Splitter;
@@ -40,7 +43,6 @@
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.project.ListProjects;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
@@ -72,10 +74,11 @@
     requestScopeOperations.setApiUser(user.id());
     assertThatNameList(gApi.projects().list().get()).contains(project);
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     assertThatNameList(gApi.projects().list().get()).doesNotContain(project);
   }
@@ -147,7 +150,9 @@
       listProjects.displayToStream(displayOut);
 
       List<String> lines =
-          Splitter.on("\n").omitEmptyStrings().splitToList(new String(displayOut.toByteArray()));
+          Splitter.on("\n")
+              .omitEmptyStrings()
+              .splitToList(new String(displayOut.toByteArray(), UTF_8));
       assertThat(lines).isEqualTo(testProjects);
     }
   }
@@ -176,7 +181,7 @@
       listProjects.setFormat(jsonFormat);
       listProjects.displayToStream(displayOut);
 
-      String projectsJsonOutput = new String(displayOut.toByteArray());
+      String projectsJsonOutput = new String(displayOut.toByteArray(), UTF_8);
 
       Gson gson = jsonFormat.newGson();
       Set<String> projectsJsonNames = gson.fromJson(projectsJsonOutput, JsonObject.class).keySet();
@@ -334,11 +339,6 @@
   }
 
   private void assertBadRequest(ListRequest req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException expected) {
-      // Expected.
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 2bd9460..3d1a148 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -24,6 +26,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
@@ -59,6 +62,7 @@
           + "=XFeC\n"
           + "-----END PGP SIGNATURE-----";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -76,7 +80,11 @@
 
   @Test
   public void listTagsOfNonVisibleProject() throws Exception {
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     requestScopeOperations.setApiUser(user.id());
     assertThrows(
         ResourceNotFoundException.class, () -> gApi.projects().name(project.get()).tags().get());
@@ -84,7 +92,11 @@
 
   @Test
   public void getTagOfNonVisibleProject() throws Exception {
-    blockRead("refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     assertThrows(
         ResourceNotFoundException.class,
         () -> gApi.projects().name(project.get()).tag("tag").get());
@@ -162,7 +174,11 @@
     assertThat(tags.get(1).ref).isEqualTo(R_TAGS + tag2.ref);
     assertThat(tags.get(1).revision).isEqualTo(tag2.revision);
 
-    blockRead("refs/heads/hidden");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/hidden").group(REGISTERED_USERS))
+        .update();
     tags = getTags().get();
     assertThat(tags).hasSize(1);
     assertThat(tags.get(0).ref).isEqualTo(R_TAGS + tag1.ref);
@@ -257,7 +273,11 @@
 
   @Test
   public void createTagNotAllowed() throws Exception {
-    block(R_TAGS + "*", Permission.CREATE, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
     TagInput input = new TagInput();
     input.ref = "test";
     AuthException thrown = assertThrows(AuthException.class, () -> tag(input.ref).create(input));
@@ -266,7 +286,11 @@
 
   @Test
   public void createAnnotatedTagNotAllowed() throws Exception {
-    block(R_TAGS + "*", Permission.CREATE_TAG, REGISTERED_USERS);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.CREATE_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
     TagInput input = new TagInput();
     input.ref = "test";
     input.message = "annotation";
@@ -370,18 +394,17 @@
   }
 
   private void assertBadRequest(ListRefsRequest<TagInfo> req) throws Exception {
-    try {
-      req.get();
-      fail("Expected BadRequestException");
-    } catch (BadRequestException e) {
-      // Expected
-    }
+    assertThrows(BadRequestException.class, () -> req.get());
   }
 
   private void grantTagPermissions() throws Exception {
-    grant(project, R_TAGS + "*", Permission.CREATE);
-    grant(project, R_TAGS + "", Permission.DELETE);
-    grant(project, R_TAGS + "*", Permission.CREATE_TAG);
-    grant(project, R_TAGS + "*", Permission.CREATE_SIGNED_TAG);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.DELETE).ref(R_TAGS + "").group(adminGroupUuid()))
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .add(allow(Permission.CREATE_SIGNED_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
+        .update();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index 2832ee5..4aebbad 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -82,26 +82,16 @@
   }
 
   private void checkBySelfFails() throws Exception {
-    Result result = resolveAsResult("self");
-    assertThat(result.asIdSet()).isEmpty();
-    assertThat(result.isSelf()).isTrue();
-    try {
-      result.asUnique();
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
-      assertThat(e.isSelf()).isTrue();
-    }
-
-    result = resolveAsResult("me");
-    assertThat(result.asIdSet()).isEmpty();
-    assertThat(result.isSelf()).isTrue();
-    try {
-      result.asUnique();
-      assert_().fail("expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
-      assertThat(e.isSelf()).isTrue();
+    for (String input : ImmutableList.of("self", "me")) {
+      Result result = resolveAsResult(input);
+      assertThat(result.asIdSet()).isEmpty();
+      assertThat(result.isSelf()).isTrue();
+      UnresolvableAccountException thrown =
+          assertThrows(UnresolvableAccountException.class, () -> result.asUnique());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(String.format("Resolving account '%s' requires login", input));
+      assertThat(thrown.isSelf()).isTrue();
     }
   }
 
@@ -268,21 +258,18 @@
     for (String input : inputs) {
       Result result = accountResolver.resolve(input);
       assertWithMessage("results for %s (inactive)", input).that(result.asIdSet()).isEmpty();
-      try {
-        result.asUnique();
-        assert_().fail("expected UnresolvableAccountException");
-      } catch (UnresolvableAccountException e) {
-        assertThat(e)
-            .hasMessageThat()
-            .isEqualTo(
-                "Account '"
-                    + input
-                    + "' only matches inactive accounts. To use an inactive account, retry"
-                    + " with one of the following exact account IDs:\n"
-                    + id
-                    + ": "
-                    + nameEmail);
-      }
+      UnresolvableAccountException thrown =
+          assertThrows(UnresolvableAccountException.class, () -> result.asUnique());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(
+              "Account '"
+                  + input
+                  + "' only matches inactive accounts. To use an inactive account, retry"
+                  + " with one of the following exact account IDs:\n"
+                  + id
+                  + ": "
+                  + nameEmail);
       assertWithMessage("results by name or email for %s (inactive)", input)
           .that(resolveByNameOrEmail(input))
           .isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index dfb0f75..9882c77 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -30,10 +31,10 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -43,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.gerrit.server.restapi.change.GetRelated;
@@ -81,6 +81,7 @@
 
   @Inject private AccountOperations accountOperations;
   @Inject private GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   private String systemTimeZone;
@@ -611,11 +612,10 @@
 
     Account.Id accountId = accountOperations.newAccount().create();
     AccountGroup.UUID groupUuid = groupOperations.newGroup().addMember(accountId).create();
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      PermissionRule rule = Util.allow(u.getConfig(), GlobalCapability.QUERY_LIMIT, groupUuid);
-      rule.setRange(0, 2);
-      u.save();
-    }
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.QUERY_LIMIT).group(groupUuid).range(0, 2))
+        .update();
     requestScopeOperations.setApiUser(accountId);
 
     assertRelated(lastPsId, expected);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 389859c..9d65d39 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -113,7 +113,7 @@
 
   @Test
   public void respectWholeTopic() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     // Create two independent commits and push.
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
@@ -135,7 +135,7 @@
 
   @Test
   public void anonymousWholeTopic() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
     RevCommit a = commitBuilder().add("a", "1").message("change 1").create();
     pushHead(testRepo, "refs/for/master/" + name("topic"), false);
     String id1 = getChangeId(a);
@@ -157,7 +157,7 @@
 
   @Test
   public void topicChaining() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
@@ -185,7 +185,7 @@
 
   @Test
   public void respectTopicsOnAncestors() throws Exception {
-    RevCommit initialHead = getRemoteHead();
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
     String id1 = getChangeId(c1_1);
diff --git a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index 4f98dd0..46fc689 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -15,16 +15,17 @@
 package com.google.gerrit.acceptance.server.event;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,8 +33,6 @@
 import com.google.gerrit.extensions.events.CommentAddedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import org.junit.After;
 import org.junit.Before;
@@ -43,37 +42,25 @@
 public class CommentAddedEventIT extends AbstractDaemonTest {
 
   @Inject private DynamicSet<CommentAddedListener> source;
+  @Inject private ProjectOperations projectOperations;
 
   private final LabelType label =
-      category("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+      label("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
 
   private final LabelType pLabel =
-      category("CustomLabel2", value(1, "Positive"), value(0, "No score"));
+      label("CustomLabel2", value(1, "Positive"), value(0, "No score"));
 
   private RegistrationHandle eventListenerRegistration;
   private CommentAddedListener.Event lastCommentAddedEvent;
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(label.getName()),
-          -1,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(pLabel.getName()),
-          0,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      u.save();
-    }
-
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(pLabel.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(0, 1))
+        .update();
     eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index abf02d5..bda2163 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.acceptance.server.mail;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.ALL;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.NONE;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER;
@@ -31,6 +34,7 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.AbstractNotificationTest;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
@@ -51,8 +55,6 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
@@ -61,6 +63,7 @@
 import org.junit.Test;
 
 public class ChangeNotificationsIT extends AbstractNotificationTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   /*
@@ -80,14 +83,14 @@
 
   @Before
   public void grantPermissions() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      ProjectConfig cfg = u.getConfig();
-      Util.allow(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.ABANDON, REGISTERED_USERS, "refs/*");
-      Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/*").group(REGISTERED_USERS))
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
   }
 
   /*
@@ -1515,15 +1518,16 @@
       }
 
       merge(sc.changeId, sc.owner);
-      assertThat(sender)
-          .named(name)
+      assertWithMessage(name)
+          .about(fakeEmailSenders())
+          .that(sender)
           .sent("merged", sc)
           .cc(sc.reviewer, sc.ccer)
           .cc(sc.reviewerByEmail, sc.ccerByEmail)
           .bcc(sc.starrer)
           .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
           .noOneElse();
-      assertThat(sender).named(name).didNotSend();
+      assertWithMessage(name).about(fakeEmailSenders()).that(sender).didNotSend();
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 708d162..5e7070b 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -127,12 +128,9 @@
               throw new ResourceConflictException(msg);
             }
           });
-      try {
-        bu.execute();
-        fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        assertThat(e).hasMessageThat().isEqualTo(msg);
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown).hasMessageThat().isEqualTo(msg);
     }
 
     // If updateChange hadn't failed, backup would have been updated to master2.
@@ -195,12 +193,7 @@
   }
 
   private void assertNoSuchChangeException(Callable<?> callable) throws Exception {
-    try {
-      callable.call();
-      fail("expected NoSuchChangeException");
-    } catch (NoSuchChangeException e) {
-      // Expected.
-    }
+    assertThrows(NoSuchChangeException.class, () -> callable.call());
   }
 
   private class ConcurrentWritingListener implements BatchUpdateListener {
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 21a7d95..f80f86b 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
 import static com.google.gerrit.common.data.LabelFunction.ANY_WITH_BLOCK;
 import static com.google.gerrit.common.data.LabelFunction.MAX_NO_BLOCK;
 import static com.google.gerrit.common.data.LabelFunction.MAX_WITH_BLOCK;
@@ -24,16 +26,17 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -42,10 +45,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import org.junit.After;
@@ -56,31 +56,24 @@
 public class CustomLabelIT extends AbstractDaemonTest {
 
   @Inject private DynamicSet<CommentAddedListener> source;
+  @Inject private ProjectOperations projectOperations;
 
   private final LabelType label =
-      category("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+      label("CustomLabel", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
 
-  private final LabelType P = category("CustomLabel2", value(1, "Positive"), value(0, "No score"));
+  private final LabelType P = label("CustomLabel2", value(1, "Positive"), value(0, "No score"));
 
   private RegistrationHandle eventListenerRegistration;
   private CommentAddedListener.Event lastCommentAddedEvent;
 
   @Before
   public void setUp() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-      Util.allow(
-          u.getConfig(),
-          Permission.forLabel(label.getName()),
-          -1,
-          1,
-          anonymousUsers,
-          "refs/heads/*");
-      Util.allow(
-          u.getConfig(), Permission.forLabel(P.getName()), 0, 1, anonymousUsers, "refs/heads/*");
-      u.save();
-    }
-
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(label.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel(P.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(0, 1))
+        .update();
     eventListenerRegistration = source.add("gerrit", event -> lastCommentAddedEvent = event);
   }
 
@@ -292,11 +285,11 @@
         value(-1, "I would prefer this is not merged as is"),
         value(-2, "This shall not be merged"));
 
-    AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.forLabel(testLabel), -2, +2, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(testLabel).ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
@@ -314,11 +307,12 @@
     assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
 
     // Update admin's permitted range for 'Test-Label' to be -1...+1.
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.remove(u.getConfig(), Permission.forLabel(testLabel), registered, "refs/heads/*");
-      Util.allow(u.getConfig(), Permission.forLabel(testLabel), -1, +1, registered, "refs/heads/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .remove(labelPermissionKey(testLabel).ref("refs/heads/*").group(REGISTERED_USERS))
+        .add(allowLabel(testLabel).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, +1))
+        .update();
 
     // Verify admin doesn't have +2 permission any more.
     assertPermitted(gApi.changes().id(changeId).get(), testLabel, -1, 0, 1);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index f3b5009..1d656ea 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
 
 import com.google.common.collect.ImmutableSet;
@@ -511,12 +512,14 @@
     // create group that can view all private changes
     GroupInfo groupThatCanViewPrivateChanges =
         gApi.groups().create("groupThatCanViewPrivateChanges").get();
-    grant(
-        Project.nameKey(watchedProject),
-        "refs/*",
-        Permission.VIEW_PRIVATE_CHANGES,
-        false,
-        AccountGroup.uuid(groupThatCanViewPrivateChanges.id));
+    projectOperations
+        .project(Project.nameKey(watchedProject))
+        .forUpdate()
+        .add(
+            allow(Permission.VIEW_PRIVATE_CHANGES)
+                .ref("refs/*")
+                .group(AccountGroup.uuid(groupThatCanViewPrivateChanges.id)))
+        .update();
 
     // watch project as user that can't view private changes
     requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index 060a9c3..a230e35 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -16,12 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -31,7 +33,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.inject.Inject;
 import java.io.File;
 import java.util.List;
@@ -41,6 +42,7 @@
 
 @UseLocalDisk
 public class ReflogIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
@@ -96,10 +98,11 @@
     GroupApi groupApi = gApi.groups().create(name("get-reflog"));
     groupApi.addMembers("user");
 
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.allow(u.getConfig(), Permission.OWNER, AccountGroup.uuid(groupApi.get().id), "refs/*");
-      u.save();
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.OWNER).ref("refs/*").group(AccountGroup.uuid(groupApi.get().id)))
+        .update();
 
     requestScopeOperations.setApiUser(user.id());
     gApi.projects().name(project.get()).branch("master").reflog();
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
index 8b9ffc5..1caaf00 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.quota;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
@@ -98,12 +99,11 @@
     replay(quotaEnforcerA);
     replay(quotaEnforcerB);
 
-    try {
-      quotaBackend.user(identifiedAdmin).requestToken("testGroup");
-      fail("expected a NullPointerException");
-    } catch (NullPointerException e) {
-      assertThat(exception).isEqualTo(e);
-    }
+    NullPointerException thrown =
+        assertThrows(
+            NullPointerException.class,
+            () -> quotaBackend.user(identifiedAdmin).requestToken("testGroup"));
+    assertThat(exception).isEqualTo(thrown);
 
     verify(quotaEnforcerA);
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index c69712c..1062052 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
@@ -41,6 +42,7 @@
   private static final String RULE_TEMPLATE =
       "submit_rule(submit(W)) :- \n" + "%s,\n" + "W = label('OK', ok(user(1000000))).";
 
+  @Inject private ProjectOperations projectOperations;
   @Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
 
   @Before
@@ -85,7 +87,7 @@
   }
 
   private SubmitRecord.Status statusForRule() throws Exception {
-    String oldHead = getRemoteHead().name();
+    String oldHead = projectOperations.project(project).getHead("master").name();
     PushOneCommit.Result result1 =
         pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 7f2abc8..eaf65ae 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -36,7 +36,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_0);
+    return getConfig(ElasticVersion.V7_1);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index fd51618..de13552 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -117,7 +118,7 @@
       // that is currently not public.
       char channel = packet.charAt(0);
       if (channel != 1) {
-        fail("got packet on channel " + (int) channel, packet);
+        assert_().fail("got packet on channel " + (int) channel, packet);
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 8ecd21c..010189b 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -15,17 +15,27 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
 import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
 import com.google.gerrit.common.data.Permission;
@@ -146,7 +156,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(TestProjectUpdate.allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -163,7 +173,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(TestProjectUpdate.deny(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(deny(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -180,7 +190,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(TestProjectUpdate.block(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(block(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -197,11 +207,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(
-            TestProjectUpdate.allow(Permission.ABANDON)
-                .ref("refs/foo")
-                .group(REGISTERED_USERS)
-                .force(true))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS).force(true))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -213,13 +219,69 @@
   }
 
   @Test
+  public void updateExclusivePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), true)
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "abandon", "group global:Registered-Users",
+            "exclusiveGroupPermissions", "abandon");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), false)
+        .update();
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addMultipleExclusivePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), true)
+        .setExclusiveGroup(permissionKey(Permission.CREATE).ref("refs/foo"), true)
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("exclusiveGroupPermissions", "abandon create");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(permissionKey(Permission.ABANDON).ref("refs/foo"), false)
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("exclusiveGroupPermissions", "create");
+  }
+
+  @Test
   public void addMultiplePermissions() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
     projectOperations
         .project(key)
         .forUpdate()
-        .add(TestProjectUpdate.allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
-        .add(TestProjectUpdate.allow(Permission.CREATE).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
+        .add(allow(Permission.CREATE).ref("refs/foo").group(REGISTERED_USERS))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -266,11 +328,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(
-            TestProjectUpdate.allowLabel("Code-Review")
-                .ref("refs/foo")
-                .group(REGISTERED_USERS)
-                .range(-1, 2))
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -287,11 +345,7 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(
-            TestProjectUpdate.blockLabel("Code-Review")
-                .ref("refs/foo")
-                .group(REGISTERED_USERS)
-                .range(-1, 2))
+        .add(blockLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -308,12 +362,8 @@
     projectOperations
         .project(key)
         .forUpdate()
-        .add(
-            TestProjectUpdate.allowLabel("Code-Review")
-                .ref("refs/foo")
-                .group(REGISTERED_USERS)
-                .range(-1, 2)
-                .exclusive(true))
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/foo"), true)
         .update();
 
     Config config = projectOperations.project(key).getConfig();
@@ -324,58 +374,212 @@
         .containsExactly(
             "label-Code-Review", "-1..+2 group global:Registered-Users",
             "exclusiveGroupPermissions", "label-Code-Review");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/foo"), false)
+        .update();
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "-1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowLabelAsPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            allowLabel("Code-Review")
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .range(-1, 2)
+                .impersonation(true))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("access");
+    assertThat(config).subsections("access").containsExactly("refs/foo");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("labelAs-Code-Review", "-1..+2 group global:Registered-Users");
   }
 
   @Test
   public void addAllowCapability() throws Exception {
-    Project.NameKey key = projectOperations.newProject().create();
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config)
+        .sectionValues("capability")
+        .doesNotContainEntry("administrateServer", "group Registered Users");
+
     projectOperations
-        .project(key)
-        .forUpdate()
+        .allProjectsForUpdate()
         .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
         .update();
 
-    Config config = projectOperations.project(key).getConfig();
-    assertThat(config).sections().containsExactly("capability");
-    assertThat(config).subsections("capability").isEmpty();
-    assertThat(config)
+    assertThat(projectOperations.project(allProjects).getConfig())
         .sectionValues("capability")
-        .containsExactly("administrateServer", "group global:Registered-Users");
+        .containsEntry("administrateServer", "group Registered Users");
   }
 
   @Test
   public void addAllowCapabilityWithRange() throws Exception {
-    Project.NameKey key = projectOperations.newProject().create();
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config).sectionValues("capability").doesNotContainKey("queryLimit");
+
     projectOperations
-        .project(key)
-        .forUpdate()
+        .allProjectsForUpdate()
         .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 5000))
         .update();
 
-    Config config = projectOperations.project(key).getConfig();
-    assertThat(config).sections().containsExactly("capability");
-    assertThat(config).subsections("capability").isEmpty();
-    assertThat(config)
+    assertThat(projectOperations.project(allProjects).getConfig())
         .sectionValues("capability")
-        .containsExactly("queryLimit", "+0..+5000 group global:Registered-Users");
+        .containsEntry("queryLimit", "+0..+5000 group Registered Users");
   }
 
   @Test
   public void addAllowCapabilityWithDefaultRange() throws Exception {
+    Config config = projectOperations.project(allProjects).getConfig();
+    assertThat(config).sectionValues("capability").doesNotContainKey("queryLimit");
+
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS))
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsEntry("queryLimit", "+0..+" + DEFAULT_MAX_QUERY_LIMIT + " group Registered Users");
+  }
+
+  @Test
+  public void removePermission() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
     projectOperations
         .project(key)
         .forUpdate()
-        .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
         .update();
-
-    Config config = projectOperations.project(key).getConfig();
-    assertThat(config).sections().containsExactly("capability");
-    assertThat(config).subsections("capability").isEmpty();
-    assertThat(config)
-        .sectionValues("capability")
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
         .containsExactly(
-            "queryLimit", "+0..+" + DEFAULT_MAX_QUERY_LIMIT + " group global:Registered-Users");
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Project-Owners");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(permissionKey(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("abandon", "group global:Project-Owners");
+  }
+
+  @Test
+  public void removeLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+        .add(allowLabel("Code-Review").ref("refs/foo").group(PROJECT_OWNERS).range(-2, 1))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly(
+            "label-Code-Review", "-1..+2 group global:Registered-Users",
+            "label-Code-Review", "-2..+1 group global:Project-Owners");
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(labelPermissionKey("Code-Review").ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsExactly("label-Code-Review", "-2..+1 group global:Project-Owners");
+  }
+
+  @Test
+  public void removeCapability() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .add(allowCapability(ADMINISTRATE_SERVER).group(PROJECT_OWNERS))
+        .update();
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .containsAtLeastEntriesIn(
+            ImmutableListMultimap.of(
+                "administrateServer", "group Registered Users",
+                "administrateServer", "group Project Owners"));
+
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(capabilityKey(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .sectionValues("capability")
+        .doesNotContainEntry("administrateServer", "group Registered Users");
+  }
+
+  @Test
+  public void removeOnePermissionForAllGroupsFromOneAccessSection() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(PROJECT_OWNERS))
+        .add(allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .add(allow(Permission.CREATE).ref("refs/foo").group(REGISTERED_USERS))
+        .update();
+    assertThat(projectOperations.project(key).getConfig())
+        .subsectionValues("access", "refs/foo")
+        .containsAtLeastEntriesIn(
+            ImmutableListMultimap.of(
+                "abandon", "group global:Project-Owners",
+                "abandon", "group global:Registered-Users",
+                "create", "group global:Registered-Users"));
+
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .remove(permissionKey(Permission.ABANDON).ref("refs/foo"))
+        .update();
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).subsectionValues("access", "refs/foo").doesNotContainKey("abandon");
+    assertThat(config)
+        .subsectionValues("access", "refs/foo")
+        .containsEntry("create", "group global:Registered-Users");
+  }
+
+  @Test
+  public void updatingCapabilitiesNotAllowedForNonAllProjects() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    assertThrows(
+        RuntimeException.class,
+        () ->
+            projectOperations
+                .project(key)
+                .forUpdate()
+                .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+                .update());
+    assertThrows(
+        RuntimeException.class,
+        () ->
+            projectOperations
+                .project(key)
+                .forUpdate()
+                .remove(capabilityKey(ADMINISTRATE_SERVER))
+                .update());
   }
 
   private void deleteRefsMetaConfig(Project.NameKey key) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java
new file mode 100644
index 0000000..b81b23d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdateTest.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
+import static com.google.gerrit.common.data.Permission.ABANDON;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermissionKey;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import java.util.function.Function;
+import org.junit.Test;
+
+public class TestProjectUpdateTest {
+  private static final AllProjectsName ALL_PROJECTS_NAME = new AllProjectsName("All-Projects");
+
+  @Test
+  public void testCapabilityDisallowsZeroRangeOnCapabilityThatHasNoRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).range(0, 0).build());
+  }
+
+  @Test
+  public void testCapabilityAllowsZeroRangeOnCapabilityThatHasRange() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 0).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(0);
+  }
+
+  @Test
+  public void testCapabilityDisallowsInvertedRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(1, 0).build());
+  }
+
+  @Test
+  public void testCapabilityDisallowsRangeIfCapabilityDoesNotSupportRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).range(-1, 1).build());
+  }
+
+  @Test
+  public void testCapabilityRangeIsZeroIfCapabilityDoesNotSupportRange() throws Exception {
+    TestCapability c = allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(0);
+  }
+
+  @Test
+  public void testCapabilityUsesDefaultRangeIfUnspecified() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(DEFAULT_MAX_QUERY_LIMIT);
+
+    c = allowCapability(BATCH_CHANGES_LIMIT).group(REGISTERED_USERS).build();
+    assertThat(c.min()).isEqualTo(0);
+    assertThat(c.max()).isEqualTo(DEFAULT_MAX_BATCH_CHANGES_LIMIT);
+  }
+
+  @Test
+  public void testCapabilityUsesExplicitRangeIfSpecified() throws Exception {
+    TestCapability c = allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(5, 20).build();
+    assertThat(c.min()).isEqualTo(5);
+    assertThat(c.max()).isEqualTo(20);
+  }
+
+  @Test
+  public void testLabelPermissionRequiresValidLabelName() throws Exception {
+    Function<String, TestLabelPermission.Builder> labelBuilder =
+        name -> allowLabel(name).ref("refs/*").group(REGISTERED_USERS).range(-1, 1);
+    assertThat(labelBuilder.apply("Code-Review").build().name()).isEqualTo("Code-Review");
+    assertThrows(RuntimeException.class, () -> labelBuilder.apply("not a label").build());
+    assertThrows(RuntimeException.class, () -> labelBuilder.apply("label-Code-Review").build());
+  }
+
+  @Test
+  public void testLabelPermissionDisallowsZeroRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(0, 0).build());
+  }
+
+  @Test
+  public void testLabelPermissionDisallowsInvertedRange() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(1, 0).build());
+  }
+
+  @Test
+  public void testPermissionKeyRequiresValidRefName() throws Exception {
+    Function<String, TestPermissionKey.Builder> keyBuilder =
+        ref -> permissionKey(ABANDON).ref(ref).group(REGISTERED_USERS);
+    assertThat(keyBuilder.apply("refs/*").build().section()).isEqualTo("refs/*");
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply(null).build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("foo").build());
+  }
+
+  @Test
+  public void testLabelPermissionKeyRequiresValidLabelName() throws Exception {
+    Function<String, TestPermissionKey.Builder> keyBuilder =
+        label -> labelPermissionKey(label).ref("refs/*").group(REGISTERED_USERS);
+    assertThat(keyBuilder.apply("Code-Review").build().name()).isEqualTo("label-Code-Review");
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply(null).build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("not a label").build());
+    assertThrows(RuntimeException.class, () -> keyBuilder.apply("label-Code-Review").build());
+  }
+
+  @Test
+  public void testPermissionKeyDisallowsSettingRefOnGlobalCapability() throws Exception {
+    assertThrows(RuntimeException.class, () -> capabilityKey(ADMINISTRATE_SERVER).ref("refs/*"));
+  }
+
+  @Test
+  public void testProjectUpdateDisallowsGroupOnExclusiveGroupPermissionKey() throws Exception {
+    TestPermissionKey.Builder b = permissionKey(ABANDON).ref("refs/*");
+    Function<TestPermissionKey.Builder, TestProjectUpdate.Builder> updateBuilder =
+        kb -> builder().setExclusiveGroup(kb, true);
+
+    assertThat(updateBuilder.apply(b).build().exclusiveGroupPermissions())
+        .containsExactly(b.build(), true);
+
+    b.group(REGISTERED_USERS);
+    assertThrows(RuntimeException.class, () -> updateBuilder.apply(b).build());
+  }
+
+  @Test
+  public void hasCapabilityUpdates() throws Exception {
+    assertThat(builder().build().hasCapabilityUpdates()).isFalse();
+    assertThat(
+            builder()
+                .add(allow(ABANDON).ref("refs/*").group(REGISTERED_USERS))
+                .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(0, 1))
+                .remove(permissionKey(ABANDON).ref("refs/foo"))
+                .remove(labelPermissionKey("Code-Review").ref("refs/foo"))
+                .setExclusiveGroup(permissionKey(ABANDON).ref("refs/bar"), true)
+                .setExclusiveGroup(labelPermissionKey(ABANDON).ref("refs/bar"), true)
+                .build()
+                .hasCapabilityUpdates())
+        .isFalse();
+    assertThat(
+            builder(ALL_PROJECTS_NAME)
+                .add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS))
+                .build()
+                .hasCapabilityUpdates())
+        .isTrue();
+    assertThat(
+            builder(ALL_PROJECTS_NAME)
+                .remove(capabilityKey(ADMINISTRATE_SERVER))
+                .build()
+                .hasCapabilityUpdates())
+        .isTrue();
+  }
+
+  @Test
+  public void updatingCapabilitiesNotAllowedForNonAllProjects() throws Exception {
+    assertThrows(
+        RuntimeException.class,
+        () -> builder().add(allowCapability(ADMINISTRATE_SERVER).group(REGISTERED_USERS)).update());
+    assertThrows(
+        RuntimeException.class,
+        () -> builder().remove(capabilityKey(ADMINISTRATE_SERVER)).update());
+  }
+
+  private static TestProjectUpdate.Builder builder() {
+    return builder(Project.nameKey("test-project"));
+  }
+
+  private static TestProjectUpdate.Builder builder(Project.NameKey nameKey) {
+    return TestProjectUpdate.builder(nameKey, ALL_PROJECTS_NAME, u -> {});
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
index 4d0bb52..90f581d 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -68,12 +68,9 @@
   @Test
   public void setApiUserToNonExistingUser() throws Exception {
     fastCheckCurrentUser(admin.id());
-    try {
-      requestScopeOperations.setApiUser(Account.id(sequences.nextAccountId()));
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(
+        RuntimeException.class,
+        () -> requestScopeOperations.setApiUser(Account.id(sequences.nextAccountId())));
     checkCurrentUser(admin.id());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 85ed4fa..adcfcb5 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -52,6 +52,8 @@
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.7.2";
       case V7_0:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.1";
+      case V7_1:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.1.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 9aaf4bb..6802873 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.elasticsearch;
 
 import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
@@ -33,7 +32,7 @@
   }
 
   public static void configure(Config config, int port, String prefix, ElasticVersion version) {
-    config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
+    config.setString("index", null, "type", "elasticsearch");
     config.setString("elasticsearch", null, "server", "http://localhost:" + port);
     config.setString("elasticsearch", null, "prefix", prefix);
     config.setInt("index", null, "maxLimit", 10000);
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index a329c8a..78c3684 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index a2d3c6d..ae00e0d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.elasticsearch;
 
+import static java.util.concurrent.TimeUnit.MINUTES;
+
 import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
@@ -49,7 +51,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
     client = HttpAsyncClients.createDefault();
     client.start();
@@ -65,14 +67,16 @@
   @Rule public final GerritTestName testName = new GerritTestName();
 
   @After
-  public void closeIndex() {
-    client.execute(
-        new HttpPost(
-            String.format(
-                "http://localhost:%d/%s*/_close",
-                nodeInfo.port, testName.getSanitizedMethodName())),
-        HttpClientContext.create(),
-        null);
+  public void closeIndex() throws Exception {
+    client
+        .execute(
+            new HttpPost(
+                String.format(
+                    "http://localhost:%d/%s*/_close",
+                    nodeInfo.port, testName.getSanitizedMethodName())),
+            HttpClientContext.create(),
+            null)
+        .get(5, MINUTES);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index e487c56..301b5dd 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 20cc90f..e1b7e3f 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/git/ObjectIdsTest.java b/javatests/com/google/gerrit/git/ObjectIdsTest.java
index 36c10a4..ccce8ea 100644
--- a/javatests/com/google/gerrit/git/ObjectIdsTest.java
+++ b/javatests/com/google/gerrit/git/ObjectIdsTest.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.git;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
 
 import java.util.function.Function;
@@ -111,8 +112,8 @@
     assertThat(ObjectIds.matchesAbbreviation(ID, "")).isTrue();
     for (int i = 1; i <= OBJECT_ID_STRING_LENGTH; i++) {
       String prefix = ID.name().substring(0, i);
-      assertThat(ObjectIds.matchesAbbreviation(ID, prefix))
-          .named("match %s against %s", ID.name(), prefix)
+      assertWithMessage("match %s against %s", ID.name(), prefix)
+          .that(ObjectIds.matchesAbbreviation(ID, prefix))
           .isTrue();
     }
 
@@ -128,12 +129,7 @@
   }
 
   private static void assertRuntimeException(Func func) throws Exception {
-    try {
-      func.call();
-      assert_().fail("Expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> func.call());
   }
 
   private static ObjectReader newReaderWithAmbiguousIds() throws Exception {
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
index fe40fb4..1d021f7 100644
--- a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -80,23 +80,18 @@
 
   @SafeVarargs
   private static void assertIoException(Consumer<ReceiveCommand>... resultSetters) {
-    try {
-      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-      assert_().fail("expected IOException");
-    } catch (IOException e) {
-      assertThat(e).isNotInstanceOf(LockFailureException.class);
-    }
+    IOException thrown =
+        assertThrows(
+            IOException.class, () -> RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)));
+    assertThat(thrown).isNotInstanceOf(LockFailureException.class);
   }
 
   @SafeVarargs
   private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
       throws Exception {
-    try {
-      RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
-      assert_().fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      // Expected.
-    }
+    assertThrows(
+        LockFailureException.class,
+        () -> RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)));
   }
 
   @SafeVarargs
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index a1f60de..a77525e 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/query/testing",
+        "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:junit",
diff --git a/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java b/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java
new file mode 100644
index 0000000..7064f64
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/LazyDataSourceTest.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index.query;
+
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.OrSource;
+import java.util.Collection;
+import java.util.Iterator;
+import org.junit.Test;
+
+/**
+ * Tests that boolean data sources are lazy in that they don't call {@link ResultSet#toList()} or
+ * {@link ResultSet#toList()}. This is necessary because it allows Gerrit to send multiple queries
+ * to the index in parallel, have the results come in asynchronously and wait for them only when we
+ * call aforementioned methods on the {@link ResultSet}.
+ */
+public class LazyDataSourceTest {
+
+  /** Helper to avoid a mock which would be hard to create because of the type inference. */
+  static class LazyPredicate extends Predicate<ChangeData> implements ChangeDataSource {
+    @Override
+    public int getCardinality() {
+      return 1;
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() {
+      return new FailingResultSet<>();
+    }
+
+    @Override
+    public ResultSet<FieldBundle> readRaw() {
+      return new FailingResultSet<>();
+    }
+
+    @Override
+    public Predicate<ChangeData> copy(Collection<? extends Predicate<ChangeData>> children) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public int hashCode() {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public boolean hasChange() {
+      throw new UnsupportedOperationException("not implemented");
+    }
+  }
+
+  /** Implementation that throws {@link AssertionError} when accessing results. */
+  static class FailingResultSet<T> implements ResultSet<T> {
+    @Override
+    public Iterator<T> iterator() {
+      throw new AssertionError(
+          "called iterator() on the result set, but shouldn't have because the data source must be lazy");
+    }
+
+    @Override
+    public ImmutableList<T> toList() {
+      throw new AssertionError(
+          "called toList() on the result set, but shouldn't have because the data source must be lazy");
+    }
+
+    @Override
+    public void close() {
+      // No-op
+    }
+  }
+
+  @Test
+  public void andSourceIsLazy() {
+    AndSource<ChangeData> and = new AndSource<>(ImmutableList.of(new LazyPredicate()));
+    ResultSet<ChangeData> resultSet = and.read();
+    assertThrows(AssertionError.class, () -> resultSet.toList());
+  }
+
+  @Test
+  public void orSourceIsLazy() {
+    OrSource or = new OrSource(ImmutableList.of(new LazyPredicate()));
+    ResultSet<ChangeData> resultSet = or.read();
+    assertThrows(AssertionError.class, () -> resultSet.toList());
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index f315da5..776a2c4 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.index.query;
 
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.index.query.QueryParser.AND;
 import static com.google.gerrit.index.query.QueryParser.COLON;
 import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
@@ -22,6 +21,7 @@
 import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
 import static com.google.gerrit.index.query.QueryParser.parse;
 import static com.google.gerrit.index.query.testing.TreeSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.antlr.runtime.tree.Tree;
 import org.junit.Test;
@@ -205,11 +205,6 @@
   }
 
   private static void assertParseFails(String query) {
-    try {
-      parse(query);
-      assert_().fail("expected parse to fail: %s", query);
-    } catch (QueryParseException e) {
-      // Expected.
-    }
+    assertThrows(QueryParseException.class, () -> parse(query));
   }
 }
diff --git a/javatests/com/google/gerrit/json/BUILD b/javatests/com/google/gerrit/json/BUILD
index 4894cdb..2d95652 100644
--- a/javatests/com/google/gerrit/json/BUILD
+++ b/javatests/com/google/gerrit/json/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/json",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
index a8488a9..05a9cfb 100644
--- a/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
+++ b/javatests/com/google/gerrit/json/JavaSqlTimestampHelperTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.json;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.json.JavaSqlTimestampHelper.parseTimestamp;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import java.text.SimpleDateFormat;
 import java.util.TimeZone;
@@ -73,12 +73,7 @@
   }
 
   private static void assertInvalid(String input) {
-    try {
-      parseTimestamp(input);
-      assert_().fail("Expected IllegalArgumentException for: " + input);
-    } catch (IllegalArgumentException e) {
-      // Expected;
-    }
+    assertThrows(IllegalArgumentException.class, () -> parseTimestamp(input));
   }
 
   private String reformat(String input) {
diff --git a/javatests/com/google/gerrit/mail/AddressTest.java b/javatests/com/google/gerrit/mail/AddressTest.java
index 5607ae9..da26123 100644
--- a/javatests/com/google/gerrit/mail/AddressTest.java
+++ b/javatests/com/google/gerrit/mail/AddressTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.mail;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.junit.Test;
 
@@ -96,12 +96,9 @@
   }
 
   private void assertInvalid(String in) {
-    try {
-      Address.parse(in);
-      fail("Expected IllegalArgumentException for " + in);
-    } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Invalid email address: " + in);
-    }
+    IllegalArgumentException thrown =
+        assertThrows(IllegalArgumentException.class, () -> Address.parse(in));
+    assertThat(thrown).hasMessageThat().isEqualTo("Invalid email address: " + in);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/proto/ProtosTest.java b/javatests/com/google/gerrit/proto/ProtosTest.java
index edaca54..550bcc5 100644
--- a/javatests/com/google/gerrit/proto/ProtosTest.java
+++ b/javatests/com/google/gerrit/proto/ProtosTest.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.proto;
 
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
@@ -33,23 +33,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     byte[] bytes = Protos.toByteArray(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes));
   }
 
   @Test
   public void parseUncheckedByteArrayInvalidData() {
     byte[] bytes = new byte[] {0x00};
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes));
   }
 
   @Test
@@ -73,23 +67,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     byte[] bytes = Protos.toByteArray(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length));
   }
 
   @Test
   public void parseUncheckedSegmentOfByteArrayInvalidData() {
     byte[] bytes = new byte[] {0x00};
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), bytes, 0, bytes.length));
   }
 
   @Test
@@ -122,23 +110,17 @@
             .setId(ByteString.copyFromUtf8("foo"))
             .build();
     ByteString byteString = Protos.toByteString(proto);
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString));
   }
 
   @Test
   public void parseUncheckedByteStringInvalidData() {
     ByteString byteString = ByteString.copyFrom(new byte[] {0x00});
-    try {
-      Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> Protos.parseUnchecked(ChangeNotesStateProto.parser(), byteString));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java b/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 2167bcd..c195533 100644
--- a/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/javatests/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.reviewdb.client;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.reviewdb.client.PatchSet.joinGroups;
 import static com.google.gerrit.reviewdb.client.PatchSet.splitGroups;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
@@ -126,11 +126,6 @@
   }
 
   private static void assertRuntimeException(Runnable runnable) {
-    try {
-      runnable.run();
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> runnable.run());
   }
 }
diff --git a/javatests/com/google/gerrit/reviewdb/client/PatchTest.java b/javatests/com/google/gerrit/reviewdb/client/PatchTest.java
index 2939a9e..d9a30e5 100644
--- a/javatests/com/google/gerrit/reviewdb/client/PatchTest.java
+++ b/javatests/com/google/gerrit/reviewdb/client/PatchTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.reviewdb.client;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.junit.Test;
 
@@ -49,11 +49,6 @@
   }
 
   private static void assertInvalidKey(String str) {
-    try {
-      Patch.Key.parse(str);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> Patch.Key.parse(str));
   }
 }
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 5e3b35f..5066368 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -34,6 +34,7 @@
     ],
     deps = [
         ":custom-truth-subjects",
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/exceptions",
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index f03d60d..d4ad7d7 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableList;
@@ -36,7 +36,7 @@
 import org.junit.Test;
 
 public class AccountResolverTest {
-  private class TestSearcher extends StringSearcher {
+  private static class TestSearcher extends StringSearcher {
     private final String pattern;
     private final boolean shortCircuit;
     private final ImmutableList<AccountState> accounts;
@@ -224,15 +224,14 @@
 
   @Test
   public void asUniqueWithNoResults() throws Exception {
-    try {
-      String input = "foo";
-      ImmutableList<Searcher<?>> searchers = ImmutableList.of();
-      Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
-      search(input, searchers, visibilitySupplier).asUnique();
-      assert_().fail("Expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e).hasMessageThat().isEqualTo("Account 'foo' not found");
-    }
+    String input = "foo";
+    ImmutableList<Searcher<?>> searchers = ImmutableList.of();
+    Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
+    UnresolvableAccountException thrown =
+        assertThrows(
+            UnresolvableAccountException.class,
+            () -> search(input, searchers, visibilitySupplier).asUnique());
+    assertThat(thrown).hasMessageThat().isEqualTo("Account 'foo' not found");
   }
 
   @Test
@@ -248,14 +247,13 @@
   public void asUniqueWithMultipleResults() throws Exception {
     ImmutableList<Searcher<?>> searchers =
         ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
-    try {
-      search("foo", searchers, allVisible()).asUnique();
-      assert_().fail("Expected UnresolvableAccountException");
-    } catch (UnresolvableAccountException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
-    }
+    UnresolvableAccountException thrown =
+        assertThrows(
+            UnresolvableAccountException.class,
+            () -> search("foo", searchers, allVisible()).asUnique());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
index 7504850..ebd7d55 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
@@ -15,10 +15,9 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.protobuf.TextFormat;
 import org.junit.Test;
 
 public class BooleanCacheSerializerTest {
@@ -52,11 +51,6 @@
   }
 
   private static void assertDeserializeFails(byte[] in) {
-    try {
-      BooleanCacheSerializer.INSTANCE.deserialize(in);
-      assert_().fail("expected deserialization to fail for \"%s\"", TextFormat.escapeBytes(in));
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> BooleanCacheSerializer.INSTANCE.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
index ac334d2..819189f 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/CacheSerializerTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Converter;
@@ -45,11 +45,6 @@
 
   @Test
   public void deserializeNullFails() throws Exception {
-    try {
-      SERIALIZER.deserialize(null);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> SERIALIZER.deserialize(null));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
index 0b80fc7..7bfcc59 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import org.junit.Test;
@@ -49,11 +49,6 @@
 
   private static void assertDeserializeFails(byte[] in) {
     CacheSerializer<MyEnum> s = new EnumCacheSerializer<>(MyEnum.class);
-    try {
-      s.deserialize(in);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> s.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
index dfd23e6..40ff0ac 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Bytes;
@@ -54,11 +54,6 @@
   }
 
   private static void assertDeserializeFails(byte[] in) {
-    try {
-      IntegerCacheSerializer.INSTANCE.deserialize(in);
-      assert_().fail("expected RuntimeException");
-    } catch (RuntimeException e) {
-      // Expected.
-    }
+    assertThrows(RuntimeException.class, () -> IntegerCacheSerializer.INSTANCE.deserialize(in));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
index c56f8f8..7d6647a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdCacheSerializerTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -46,11 +46,7 @@
   }
 
   private void assertDeserializeFails(byte[] bytes) {
-    try {
-      ObjectIdCacheSerializer.INSTANCE.deserialize(bytes);
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class, () -> ObjectIdCacheSerializer.INSTANCE.deserialize(bytes));
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
index c8c80b4..f6d6c8a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ObjectIdConverterTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.protobuf.ByteString;
 import org.eclipse.jgit.lib.ObjectId;
@@ -42,12 +42,9 @@
 
   @Test
   public void objectIdFromByteStringWrongSize() {
-    try {
-      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo")));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
index fa3b7d7..dc22805 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.cache.serialize;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.StandardCharsets;
@@ -34,12 +34,11 @@
   @Test
   public void serializeInvalidChar() {
     // Can't use UTF-8 for the test, since it can encode all Unicode code points.
-    try {
-      StringCacheSerializer.serialize(StandardCharsets.US_ASCII, "\u1234");
-      assert_().fail("expected IllegalStateException");
-    } catch (IllegalStateException expected) {
-      assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
-    }
+    IllegalStateException thrown =
+        assertThrows(
+            IllegalStateException.class,
+            () -> StringCacheSerializer.serialize(StandardCharsets.US_ASCII, "\u1234"));
+    assertThat(thrown).hasCauseThat().isInstanceOf(CharacterCodingException.class);
   }
 
   @Test
@@ -55,11 +54,10 @@
 
   @Test
   public void deserializeInvalidChar() {
-    try {
-      StringCacheSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff});
-      assert_().fail("expected IllegalStateException");
-    } catch (IllegalStateException expected) {
-      assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
-    }
+    IllegalStateException thrown =
+        assertThrows(
+            IllegalStateException.class,
+            () -> StringCacheSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff}));
+    assertThat(thrown).hasCauseThat().isInstanceOf(CharacterCodingException.class);
   }
 }
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 85559cb..beeca21 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.common.data.Permission.forLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -69,6 +70,7 @@
   @Inject private ChangeNotes.Factory changeNotesFactory;
   @Inject private ProjectConfig.Factory projectConfigFactory;
   @Inject private GerritApi gApi;
+  @Inject private ProjectOperations projectOperations;
 
   private LifecycleManager lifecycle;
   private Account.Id userId;
@@ -102,7 +104,7 @@
       }
     }
     LabelType lt =
-        category("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
+        label("Verified", value(1, "Verified"), value(0, "No score"), value(-1, "Fails"));
     pc.getLabelSections().put(lt.getName(), lt);
     save(pc);
   }
@@ -128,10 +130,11 @@
 
   @Test
   public void noNormalizeByPermission() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    allow(pc, forLabel("Verified"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .add(allowLabel("Verified").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 2);
     PatchSetApproval v = psa(userId, "Verified", 1);
@@ -140,10 +143,11 @@
 
   @Test
   public void normalizeByType() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -5, 5, REGISTERED_USERS, "refs/heads/*");
-    allow(pc, forLabel("Verified"), -5, 5, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-5, 5))
+        .add(allowLabel("Verified").ref("refs/heads/*").group(REGISTERED_USERS).range(-5, 5))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 5);
     PatchSetApproval v = psa(userId, "Verified", 5);
@@ -161,9 +165,10 @@
 
   @Test
   public void explicitZeroVoteOnNonEmptyRangeIsPresent() throws Exception {
-    ProjectConfig pc = loadAllProjects();
-    allow(pc, forLabel("Code-Review"), -1, 1, REGISTERED_USERS, "refs/heads/*");
-    save(pc);
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     PatchSetApproval cr = psa(userId, "Code-Review", 0);
     PatchSetApproval v = psa(userId, "Verified", 0);
diff --git a/javatests/com/google/gerrit/server/git/JGitConfigTest.java b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
index 7cb5a98..9f6b47e 100644
--- a/javatests/com/google/gerrit/server/git/JGitConfigTest.java
+++ b/javatests/com/google/gerrit/server/git/JGitConfigTest.java
@@ -22,7 +22,11 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -52,4 +56,29 @@
       assertThat(repo.getConfig().getString("core", null, "trustFolderStat")).isEqualTo("false");
     }
   }
+
+  @Test
+  public void openSystemConfigRespectsParent() throws Exception {
+    Config parent = new Config();
+    parent.setString("foo", null, "bar", "value");
+    FileBasedConfig system = SystemReader.getInstance().openSystemConfig(parent, FS.DETECTED);
+    system.load();
+    assertThat(system.getString("core", null, "trustFolderStat")).isEqualTo("false");
+    assertThat(system.getString("foo", null, "bar")).isEqualTo("value");
+  }
+
+  @Test
+  public void openSystemConfigReturnsDifferentInstances() throws Exception {
+    FileBasedConfig system1 = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED);
+    system1.load();
+    assertThat(system1.getString("core", null, "trustFolderStat")).isEqualTo("false");
+
+    FileBasedConfig system2 = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED);
+    system2.load();
+    assertThat(system2.getString("core", null, "trustFolderStat")).isEqualTo("false");
+
+    system1.setString("core", null, "trustFolderStat", "true");
+    assertThat(system1.getString("core", null, "trustFolderStat")).isEqualTo("true");
+    assertThat(system2.getString("core", null, "trustFolderStat")).isEqualTo("false");
+  }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 3bcc199..85567dd 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.group.db;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.common.data.testing.GroupReferenceSubject.groupReferences;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
@@ -578,12 +577,13 @@
     try (ObjectInserter inserter = repo.newObjectInserter()) {
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
       PersonIdent ident = newPersonIdent();
-      try {
-        GroupNameNotes.updateAllGroups(repo, inserter, bru, Arrays.asList(groupRefs), ident);
-        assert_().fail("Expected IllegalArgumentException");
-      } catch (IllegalArgumentException e) {
-        assertThat(e).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
-      }
+      IllegalArgumentException thrown =
+          assertThrows(
+              IllegalArgumentException.class,
+              () ->
+                  GroupNameNotes.updateAllGroups(
+                      repo, inserter, bru, Arrays.asList(groupRefs), ident));
+      assertThat(thrown).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index 59e8f10..44f33b2 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.index.change.StalenessChecker.refsAreStale;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
 
@@ -82,12 +82,7 @@
   }
 
   private static void assertInvalidState(String state) {
-    try {
-      RefState.parseStates(byteArrays(state));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(IllegalArgumentException.class, () -> RefState.parseStates(byteArrays(state)));
   }
 
   @Test
@@ -154,12 +149,8 @@
   }
 
   private static void assertInvalidPattern(String state) {
-    try {
-      StalenessChecker.parsePatterns(byteArrays(state));
-      assert_().fail("expected IllegalArgumentException");
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(
+        IllegalArgumentException.class, () -> StalenessChecker.parsePatterns(byteArrays(state)));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
index 4fadbb4..f6f3b46 100644
--- a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
+++ b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.logging;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -166,11 +166,7 @@
   }
 
   private void assertNullPointerException(String expectedMessage, Runnable r) {
-    try {
-      r.run();
-      assert_().fail("expected NullPointerException");
-    } catch (NullPointerException e) {
-      assertThat(e.getMessage()).isEqualTo(expectedMessage);
-    }
+    NullPointerException thrown = assertThrows(NullPointerException.class, () -> r.run());
+    assertThat(thrown).hasMessageThat().isEqualTo(expectedMessage);
   }
 }
diff --git a/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java b/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
index 885f7cd..b87c4a1 100644
--- a/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
@@ -15,27 +15,31 @@
 package com.google.gerrit.server.mail.send;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.mail.send.NotificationEmail.getInstanceAndProjectName;
+import static com.google.gerrit.server.mail.send.NotificationEmail.getShortProjectName;
 
 import org.junit.Test;
 
 public class NotificationEmailTest {
-
   @Test
-  public void getInstanceAndProjectName_returnsTheRightValue() {
-    String instanceAndProjectName = NotificationEmail.getInstanceAndProjectName("test", "/my/api");
-    assertThat(instanceAndProjectName).isEqualTo("test/api");
+  public void instanceAndProjectName() throws Exception {
+    assertThat(getInstanceAndProjectName("test", "/my/api")).isEqualTo("test/api");
+    assertThat(getInstanceAndProjectName("test", "/api")).isEqualTo("test/api");
+    assertThat(getInstanceAndProjectName("test", "api")).isEqualTo("test/api");
   }
 
   @Test
-  public void getInstanceAndProjectName_handlesNull() {
-    String instanceAndProjectName = NotificationEmail.getInstanceAndProjectName(null, "/my/api");
-    assertThat(instanceAndProjectName).isEqualTo("...api");
+  public void instanceAndProjectNameNull() throws Exception {
+    assertThat(getInstanceAndProjectName(null, "/my/api")).isEqualTo("...api");
+    assertThat(getInstanceAndProjectName(null, "/api")).isEqualTo("api");
+    assertThat(getInstanceAndProjectName(null, "api")).isEqualTo("api");
   }
 
   @Test
-  public void getShortProjectName() {
-    assertThat(NotificationEmail.getShortProjectName("/api")).isEqualTo("api");
-    assertThat(NotificationEmail.getShortProjectName("/my/api")).isEqualTo("...api");
-    assertThat(NotificationEmail.getShortProjectName("/my/sub/project")).isEqualTo("...project");
+  public void shortProjectName() throws Exception {
+    assertThat(getShortProjectName("api")).isEqualTo("api");
+    assertThat(getShortProjectName("/api")).isEqualTo("api");
+    assertThat(getShortProjectName("/my/api")).isEqualTo("...api");
+    assertThat(getShortProjectName("/my/sub/project")).isEqualTo("...project");
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 79dcd5b..52000f5 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -545,12 +545,7 @@
   }
 
   private void assertParseFails(RevCommit commit) throws Exception {
-    try {
-      newParser(commit).parseAll();
-      fail("Expected parse to fail:\n" + commit.getFullMessage());
-    } catch (ConfigInvalidException e) {
-      // Expected
-    }
+    assertThrows(ConfigInvalidException.class, () -> newParser(commit).parseAll());
   }
 
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
index 7ddc86f..201f94f 100644
--- a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
+++ b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.gerrit.exceptions.StorageException;
@@ -59,12 +59,7 @@
   public void parseNonBlob() throws Exception {
     String refName = "refs/foo/master";
     tr.branch(refName).commit().create();
-    try {
-      IntBlob.parse(repo, refName);
-      assert_().fail("Expected IncorrectObjectTypeException");
-    } catch (IncorrectObjectTypeException e) {
-      // Expected.
-    }
+    assertThrows(IncorrectObjectTypeException.class, () -> IntBlob.parse(repo, refName));
   }
 
   @Test
@@ -85,12 +80,9 @@
   public void parseInvalid() throws Exception {
     String refName = "refs/foo";
     ObjectId id = tr.update(refName, tr.blob("1 2 3"));
-    try {
-      IntBlob.parse(repo, refName);
-      assert_().fail("Expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().isEqualTo("invalid value in refs/foo blob at " + id.name());
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> IntBlob.parse(repo, refName));
+    assertThat(thrown).hasMessageThat().isEqualTo("invalid value in refs/foo blob at " + id.name());
   }
 
   @Test
@@ -180,19 +172,19 @@
   @Test
   public void storeWrongOldId() throws Exception {
     String refName = "refs/foo";
-    try {
-      IntBlob.store(
-          repo,
-          rw,
-          projectName,
-          refName,
-          ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
-          123,
-          GitReferenceUpdated.DISABLED);
-      assert_().fail("expected LockFailureException");
-    } catch (LockFailureException e) {
-      assertThat(e.getFailedRefs()).containsExactly("refs/foo");
-    }
+    LockFailureException thrown =
+        assertThrows(
+            LockFailureException.class,
+            () ->
+                IntBlob.store(
+                    repo,
+                    rw,
+                    projectName,
+                    refName,
+                    ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+                    123,
+                    GitReferenceUpdated.DISABLED));
+    assertThat(thrown.getFailedRefs()).containsExactly("refs/foo");
     assertThat(IntBlob.parse(repo, refName)).isEmpty();
   }
 
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 761d682..544d655 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -16,53 +16,44 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.common.data.Permission.OWNER;
 import static com.google.gerrit.common.data.Permission.PUSH;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.ADMIN;
-import static com.google.gerrit.server.project.testing.Util.DEVS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.allowExclusive;
-import static com.google.gerrit.server.project.testing.Util.block;
-import static com.google.gerrit.server.project.testing.Util.deny;
-import static com.google.gerrit.server.project.testing.Util.doNotInherit;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.testing.InMemoryRepositoryManager.newRepository;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.exceptions.InvalidNameException;
-import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
-import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefPattern;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.InMemoryModule;
@@ -70,25 +61,21 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class RefControlTest {
-  private void assertAdminsAreOwnersAndDevsAreNot() {
-    ProjectControl uBlah = user(local, DEVS);
-    ProjectControl uAdmin = user(local, DEVS, ADMIN);
+  private static final AccountGroup.UUID ADMIN = AccountGroup.uuid("test.admin");
+  private static final AccountGroup.UUID DEVS = AccountGroup.uuid("test.devs");
+
+  private void assertAdminsAreOwnersAndDevsAreNot() throws Exception {
+    ProjectControl uBlah = user(localKey, DEVS);
+    ProjectControl uAdmin = user(localKey, DEVS, ADMIN);
 
     assertWithMessage("not owner").that(uBlah.isOwner()).isFalse();
     assertWithMessage("is owner").that(uAdmin.isOwner()).isTrue();
@@ -178,106 +165,29 @@
     assertWithMessage("cannot vote " + score).that(range.contains(score)).isFalse();
   }
 
-  private final AllProjectsName allProjectsName =
-      new AllProjectsName(AllProjectsNameProvider.DEFAULT);
-  private final AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
   private final AccountGroup.UUID fixers = AccountGroup.uuid("test.fixers");
-  private final Map<Project.NameKey, ProjectState> all = new HashMap<>();
-  private Project.NameKey localKey = Project.nameKey("local");
-  private ProjectConfig local;
-  private Project.NameKey parentKey = Project.nameKey("parent");
-  private ProjectConfig parent;
-  private InMemoryRepositoryManager repoManager;
-  private ProjectCache projectCache;
-  private PermissionCollection.Factory sectionSorter;
-  private ChangeControl.Factory changeControlFactory;
+  private final Project.NameKey localKey = Project.nameKey("local");
+  private final Project.NameKey parentKey = Project.nameKey("parent");
 
-  @Inject private PermissionBackend permissionBackend;
-  @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
+  @Inject private AllProjectsName allProjectsName;
+  @Inject private InMemoryRepositoryManager repoManager;
+  @Inject private MetaDataUpdate.Server metaDataUpdateFactory;
+  @Inject private ProjectCache projectCache;
+  @Inject private ProjectControl.Factory projectControlFactory;
+  @Inject private ProjectOperations projectOperations;
   @Inject private SchemaCreator schemaCreator;
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private DefaultRefFilter.Factory refFilterFactory;
-  @Inject private TransferConfig transferConfig;
-  @Inject private MetricMaker metricMaker;
-  @Inject private ProjectConfig.Factory projectConfigFactory;
 
   @Before
   public void setUp() throws Exception {
-    repoManager = new InMemoryRepositoryManager();
-    projectCache =
-        new ProjectCache() {
-          @Override
-          public ProjectState getAllProjects() {
-            return get(allProjectsName);
-          }
-
-          @Override
-          public ProjectState getAllUsers() {
-            return null;
-          }
-
-          @Override
-          public ProjectState get(Project.NameKey projectName) {
-            return all.get(projectName);
-          }
-
-          @Override
-          public void evict(Project p) {}
-
-          @Override
-          public void remove(Project p) {}
-
-          @Override
-          public void remove(Project.NameKey name) {}
-
-          @Override
-          public ImmutableSortedSet<Project.NameKey> all() {
-            return ImmutableSortedSet.of();
-          }
-
-          @Override
-          public ImmutableSortedSet<Project.NameKey> byName(String prefix) {
-            return ImmutableSortedSet.of();
-          }
-
-          @Override
-          public void onCreateProject(Project.NameKey newProjectName) {}
-
-          @Override
-          public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-            return Collections.emptySet();
-          }
-
-          @Override
-          public ProjectState checkedGet(Project.NameKey projectName) throws IOException {
-            return all.get(projectName);
-          }
-
-          @Override
-          public void evict(Project.NameKey p) {}
-
-          @Override
-          public ProjectState checkedGet(Project.NameKey projectName, boolean strict)
-              throws Exception {
-            return all.get(projectName);
-          }
-        };
-
     Injector injector = Guice.createInjector(new InMemoryModule());
     injector.injectMembers(this);
 
-    try {
-      Repository repo = repoManager.createRepository(allProjectsName);
-      ProjectConfig allProjects =
-          projectConfigFactory.create(Project.nameKey(allProjectsName.get()));
-      allProjects.load(repo);
-      LabelType cr = Util.codeReview();
-      allProjects.getLabelSections().put(cr.getName(), cr);
-      add(allProjects);
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e);
-    }
+    // Tests previously used ProjectConfig.Factory to create ProjectConfigs without going through
+    // the ProjectCache, which was wrong. Manually call getInstance so we don't store it in a
+    // field that is accessible to test methods.
+    ProjectConfig.Factory projectConfigFactory = injector.getInstance(ProjectConfig.Factory.class);
 
     singleVersionListener.start();
     try {
@@ -286,58 +196,79 @@
       singleVersionListener.stop();
     }
 
-    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
-        CacheBuilder.newBuilder().build();
-    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c), metricMaker);
+    // Clear out All-Projects and use the lowest-level API possible for project creation, so the
+    // only ACL entries are exactly what is initialized by this test, and we aren't subject to
+    // changing defaults in SchemaCreator or ProjectCreator.
+    try (Repository allProjectsRepo = repoManager.createRepository(allProjectsName)) {
+      new TestRepository<>(allProjectsRepo).delete(REFS_CONFIG);
+      try (MetaDataUpdate md = metaDataUpdateFactory.create(allProjectsName)) {
+        ProjectConfig allProjectsConfig = projectConfigFactory.create(allProjectsName);
+        allProjectsConfig.load(md);
+        LabelType cr = TestLabels.codeReview();
+        allProjectsConfig.getLabelSections().put(cr.getName(), cr);
+        allProjectsConfig.commit(md);
+      }
+    }
 
-    parent = projectConfigFactory.create(parentKey);
-    parent.load(newRepository(parentKey));
-    add(parent);
-
-    local = projectConfigFactory.create(localKey);
-    local.load(newRepository(localKey));
-    add(local);
-    local.getProject().setParentName(parentKey);
+    repoManager.createRepository(parentKey).close();
+    repoManager.createRepository(localKey).close();
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(localKey)) {
+      ProjectConfig newLocal = projectConfigFactory.create(localKey);
+      newLocal.load(md);
+      newLocal.getProject().setParentName(parentKey);
+      newLocal.commit(md);
+    }
 
     requestContext.setContext(() -> null);
-
-    changeControlFactory = injector.getInstance(ChangeControl.Factory.class);
   }
 
   @After
-  public void tearDown() {
+  public void tearDown() throws Exception {
     requestContext.setContext(null);
   }
 
   @Test
-  public void ownerProject() {
-    allow(local, OWNER, ADMIN, "refs/*");
-
+  public void ownerProject() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
-  public void denyOwnerProject() {
-    allow(local, OWNER, ADMIN, "refs/*");
-    deny(local, OWNER, DEVS, "refs/*");
-
+  public void denyOwnerProject() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(deny(OWNER).ref("refs/*").group(DEVS))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
-  public void blockOwnerProject() {
-    allow(local, OWNER, ADMIN, "refs/*");
-    block(local, OWNER, DEVS, "refs/*");
-
+  public void blockOwnerProject() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(block(OWNER).ref("refs/*").group(DEVS))
+        .update();
     assertAdminsAreOwnersAndDevsAreNot();
   }
 
   @Test
-  public void branchDelegation1() {
-    allow(local, OWNER, ADMIN, "refs/*");
-    allow(local, OWNER, DEVS, "refs/heads/x/*");
+  public void branchDelegation1() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(allow(OWNER).ref("refs/heads/x/*").group(DEVS))
+        .update();
 
-    ProjectControl uDev = user(local, DEVS);
+    ProjectControl uDev = user(localKey, DEVS);
     assertNotOwner(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
@@ -349,13 +280,17 @@
   }
 
   @Test
-  public void branchDelegation2() {
-    allow(local, OWNER, ADMIN, "refs/*");
-    allow(local, OWNER, DEVS, "refs/heads/x/*");
-    allow(local, OWNER, fixers, "refs/heads/x/y/*");
-    doNotInherit(local, OWNER, "refs/heads/x/y/*");
+  public void branchDelegation2() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(ADMIN))
+        .add(allow(OWNER).ref("refs/heads/x/*").group(DEVS))
+        .add(allow(OWNER).ref("refs/heads/x/y/*").group(fixers))
+        .setExclusiveGroup(permissionKey(OWNER).ref("refs/heads/x/y/*"), true)
+        .update();
 
-    ProjectControl uDev = user(local, DEVS);
+    ProjectControl uDev = user(localKey, DEVS);
     assertNotOwner(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
@@ -364,7 +299,7 @@
     assertNotOwner("refs/*", uDev);
     assertNotOwner("refs/heads/master", uDev);
 
-    ProjectControl uFix = user(local, fixers);
+    ProjectControl uFix = user(localKey, fixers);
     assertNotOwner(uFix);
 
     assertOwner("refs/heads/x/y/*", uFix);
@@ -376,38 +311,62 @@
   }
 
   @Test
-  public void inheritRead_SingleBranchDeniesUpload() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
-    doNotInherit(local, READ, "refs/heads/foobar");
-    doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
+  public void inheritRead_SingleBranchDeniesUpload() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(PUSH).ref("refs/for/refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/foobar").group(REGISTERED_USERS))
+        .setExclusiveGroup(permissionKey(READ).ref("refs/heads/foobar"), true)
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/for/refs/heads/foobar"), true)
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanUpload(u);
     assertCreateChange("refs/heads/master", u);
     assertCannotCreateChange("refs/heads/foobar", u);
   }
 
   @Test
-  public void blockPushDrafts() {
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    allow(local, PUSH, REGISTERED_USERS, "refs/drafts/*");
+  public void blockPushDrafts() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/for/refs/*").group(REGISTERED_USERS))
+        .add(block(PUSH).ref("refs/drafts/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/drafts/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCreateChange("refs/heads/master", u);
     assertThat(u.controlForRef("refs/drafts/master").canPerform(PUSH)).isFalse();
   }
 
   @Test
-  public void blockPushDraftsUnblockAdmin() {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    allow(parent, PUSH, ADMIN, "refs/drafts/*");
-    allow(local, PUSH, REGISTERED_USERS, "refs/drafts/*");
+  public void blockPushDraftsUnblockAdmin() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/drafts/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/drafts/*").group(ADMIN))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/drafts/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
-    ProjectControl a = user(local, "a", ADMIN);
+    ProjectControl u = user(localKey);
+    ProjectControl a = user(localKey, "a", ADMIN);
 
     assertWithMessage("push is allowed")
         .that(a.controlForRef("refs/drafts/master").canPerform(PUSH))
@@ -418,12 +377,20 @@
   }
 
   @Test
-  public void inheritRead_SingleBranchDoesNotOverrideInherited() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
+  public void inheritRead_SingleBranchDoesNotOverrideInherited() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(PUSH).ref("refs/for/refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/foobar").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanUpload(u);
     assertCreateChange("refs/heads/master", u);
     assertCreateChange("refs/heads/foobar", u);
@@ -431,31 +398,50 @@
 
   @Test
   public void inheritDuplicateSections() throws Exception {
-    allow(parent, READ, ADMIN, "refs/*");
-    allow(local, READ, DEVS, "refs/heads/*");
-    assertCanAccess(user(local, "a", ADMIN));
-
-    local = projectConfigFactory.create(localKey);
-    local.load(newRepository(localKey));
-    local.getProject().setParentName(parentKey);
-    allow(local, READ, DEVS, "refs/*");
-    assertCanAccess(user(local, "d", DEVS));
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(DEVS))
+        .update();
+    assertCanAccess(user(localKey, "a", ADMIN));
+    assertCanAccess(user(localKey, "d", DEVS));
   }
 
   @Test
-  public void inheritRead_OverrideWithDeny() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/*");
+  public void inheritRead_OverrideWithDeny() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
-    assertAccessDenied(user(local));
+    assertAccessDenied(user(localKey));
   }
 
   @Test
-  public void inheritRead_AppendWithDenyOfRef() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/heads/*");
+  public void inheritRead_AppendWithDenyOfRef() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanAccess(u);
     assertCanRead("refs/master", u);
     assertCanRead("refs/tags/foobar", u);
@@ -463,12 +449,20 @@
   }
 
   @Test
-  public void inheritRead_OverridesAndDeniesOfRef() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    deny(local, READ, REGISTERED_USERS, "refs/*");
-    allow(local, READ, REGISTERED_USERS, "refs/heads/*");
+  public void inheritRead_OverridesAndDeniesOfRef() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(READ).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCanAccess(u);
     assertCannotRead("refs/foobar", u);
     assertCannotRead("refs/tags/foobar", u);
@@ -476,100 +470,164 @@
   }
 
   @Test
-  public void inheritSubmit_OverridesAndDeniesOfRef() {
-    allow(parent, SUBMIT, REGISTERED_USERS, "refs/*");
-    deny(local, SUBMIT, REGISTERED_USERS, "refs/*");
-    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+  public void inheritSubmit_OverridesAndDeniesOfRef() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(deny(SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCannotSubmit("refs/foobar", u);
     assertCannotSubmit("refs/tags/foobar", u);
     assertCanSubmit("refs/heads/foobar", u);
   }
 
   @Test
-  public void cannotUploadToAnyRef() {
-    allow(parent, READ, REGISTERED_USERS, "refs/*");
-    allow(local, READ, DEVS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/for/refs/heads/*");
+  public void cannotUploadToAnyRef() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/*").group(DEVS))
+        .add(allow(PUSH).ref("refs/for/refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertCannotUpload(u);
     assertCannotCreateChange("refs/heads/master", u);
   }
 
   @Test
-  public void usernamePatternCanUploadToAnyRef() {
-    allow(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*");
-    ProjectControl u = user(local, "a-registered-user");
+  public void usernamePatternCanUploadToAnyRef() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/users/${username}/*").group(REGISTERED_USERS))
+        .update();
+    ProjectControl u = user(localKey, "a-registered-user");
     assertCanUpload(u);
   }
 
   @Test
-  public void usernamePatternNonRegex() {
-    allow(local, READ, DEVS, "refs/sb/${username}/heads/*");
+  public void usernamePatternNonRegex() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/sb/${username}/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "u", DEVS);
-    ProjectControl d = user(local, "d", DEVS);
+    ProjectControl u = user(localKey, "u", DEVS);
+    ProjectControl d = user(localKey, "d", DEVS);
     assertCannotRead("refs/sb/d/heads/foobar", u);
     assertCanRead("refs/sb/d/heads/foobar", d);
   }
 
   @Test
-  public void usernamePatternWithRegex() {
-    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+  public void usernamePatternWithRegex() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/sb/${username}/heads/.*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "d.v", DEVS);
-    ProjectControl d = user(local, "dev", DEVS);
+    ProjectControl u = user(localKey, "d.v", DEVS);
+    ProjectControl d = user(localKey, "dev", DEVS);
     assertCannotRead("refs/sb/dev/heads/foobar", u);
     assertCanRead("refs/sb/dev/heads/foobar", d);
   }
 
   @Test
-  public void usernameEmailPatternWithRegex() {
-    allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
+  public void usernameEmailPatternWithRegex() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/sb/${username}/heads/.*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, "d.v@ger-rit.org", DEVS);
-    ProjectControl d = user(local, "dev@ger-rit.org", DEVS);
+    ProjectControl u = user(localKey, "d.v@ger-rit.org", DEVS);
+    ProjectControl d = user(localKey, "dev@ger-rit.org", DEVS);
     assertCannotRead("refs/sb/dev@ger-rit.org/heads/foobar", u);
     assertCanRead("refs/sb/dev@ger-rit.org/heads/foobar", d);
   }
 
   @Test
-  public void sortWithRegex() {
-    allow(local, READ, DEVS, "^refs/heads/.*");
-    allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
+  public void sortWithRegex() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/heads/.*").group(DEVS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(READ).ref("^refs/heads/.*-QA-.*").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
-    ProjectControl d = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
+    ProjectControl d = user(localKey, DEVS);
     assertCanRead("refs/heads/foo-QA-bar", u);
     assertCanRead("refs/heads/foo-QA-bar", d);
   }
 
   @Test
-  public void blockRule_ParentBlocksChild() {
-    allow(local, PUSH, DEVS, "refs/tags/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
-    ProjectControl u = user(local, DEVS);
+  public void blockRule_ParentBlocksChild() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/tags/*").group(DEVS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
-  public void blockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
-    allow(local, PUSH, DEVS, "refs/tags/*");
-    block(local, PUSH, ANONYMOUS_USERS, "refs/tags/*");
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
+  public void blockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/tags/*").group(DEVS))
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/tags/*").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
-  public void blockLabelRange_ParentBlocksChild() {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void blockLabelRange_ParentBlocksChild() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
@@ -579,12 +637,20 @@
   }
 
   @Test
-  public void blockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void blockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
@@ -594,251 +660,396 @@
   }
 
   @Test
-  public void inheritSubmit_AllowInChildDoesntAffectUnblockInParent() {
-    block(parent, SUBMIT, ANONYMOUS_USERS, "refs/heads/*");
-    allow(parent, SUBMIT, REGISTERED_USERS, "refs/heads/*");
-    allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
+  public void inheritSubmit_AllowInChildDoesntAffectUnblockInParent() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(SUBMIT).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(SUBMIT).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
 
-    ProjectControl u = user(local);
+    ProjectControl u = user(localKey);
     assertWithMessage("submit is allowed")
         .that(u.controlForRef("refs/heads/master").canPerform(SUBMIT))
         .isTrue();
   }
 
   @Test
-  public void unblockNoForce() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/*");
+  public void unblockNoForce() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockForce() {
-    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setForce(true);
-    allow(local, PUSH, DEVS, "refs/heads/*").setForce(true);
+  public void unblockForce() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanForceUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockRead_NotPossible() {
-    block(parent, READ, ANONYMOUS_USERS, "refs/*");
-    allow(parent, READ, ADMIN, "refs/*");
-    allow(local, READ, ANONYMOUS_USERS, "refs/*");
-    allow(local, READ, ADMIN, "refs/*");
-    ProjectControl u = user(local);
+  public void unblockRead_NotPossible() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(READ).ref("refs/*").group(ADMIN))
+        .update();
+
+    ProjectControl u = user(localKey);
     assertCannotRead("refs/heads/master", u);
   }
 
   @Test
-  public void unblockForceWithAllowNoForce_NotPossible() {
-    PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    r.setForce(true);
-    allow(local, PUSH, DEVS, "refs/heads/*");
+  public void unblockForceWithAllowNoForce_NotPossible() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS).force(true))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotForceUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockMoreSpecificRef_Fails() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
+  public void unblockMoreSpecificRef_Fails() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockMoreSpecificRefInLocal_Fails() {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
+  public void unblockMoreSpecificRefInLocal_Fails() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockMoreSpecificRefWithExclusiveFlag() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master", true);
+  public void unblockMoreSpecificRefWithExclusiveFlag() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockVoteMoreSpecificRefWithExclusiveFlag() {
-    String perm = LABEL + "Code-Review";
+  public void unblockVoteMoreSpecificRefWithExclusiveFlag() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .add(allowLabel("Code-Review").ref("refs/heads/master").group(DEVS).range(-2, 2))
+        .setExclusiveGroup(labelPermissionKey("Code-Review").ref("refs/heads/master"), true)
+        .update();
 
-    block(local, perm, -1, 1, ANONYMOUS_USERS, "refs/heads/*");
-    allowExclusive(local, perm, -2, 2, DEVS, "refs/heads/master");
-
-    ProjectControl u = user(local, DEVS);
-    PermissionRange range = u.controlForRef("refs/heads/master").getRange(perm);
+    ProjectControl u = user(localKey, DEVS);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
   }
 
   @Test
-  public void unblockFromParentDoesNotAffectChild() {
-    allow(parent, PUSH, DEVS, "refs/heads/master", true);
-    block(local, PUSH, DEVS, "refs/heads/master");
+  public void unblockFromParentDoesNotAffectChild() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockFromParentDoesNotAffectChildDifferentGroups() {
-    allow(parent, PUSH, DEVS, "refs/heads/master", true);
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
+  public void unblockFromParentDoesNotAffectChildDifferentGroups() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(ANONYMOUS_USERS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master", true);
+  public void unblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void blockMoreSpecificRefWithinProject() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/secret");
-    allow(local, PUSH, DEVS, "refs/heads/*", true);
+  public void blockMoreSpecificRefWithinProject() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/secret").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .setExclusiveGroup(permissionKey(PUSH).ref("refs/heads/*"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/secret", u);
     assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, DEVS, "refs/heads/master");
-    allow(local, SUBMIT, DEVS, "refs/heads/master", true);
+  public void unblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/master").group(DEVS))
+        .add(allow(SUBMIT).ref("refs/heads/master").group(DEVS))
+        .setExclusiveGroup(permissionKey(SUBMIT).ref("refs/heads/master"), true)
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockLargerScope_Fails() {
-    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
-    allow(local, PUSH, DEVS, "refs/heads/*");
+  public void unblockLargerScope_Fails() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/master").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
-  public void unblockInLocal_Fails() {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, PUSH, fixers, "refs/heads/*");
+  public void unblockInLocal_Fails() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(PUSH).ref("refs/heads/*").group(fixers))
+        .update();
 
-    ProjectControl f = user(local, fixers);
+    ProjectControl f = user(localKey, fixers);
     assertCannotUpdate("refs/heads/master", f);
   }
 
   @Test
-  public void unblockInParentBlockInLocal() {
-    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(parent, PUSH, DEVS, "refs/heads/*");
-    block(local, PUSH, DEVS, "refs/heads/*");
+  public void unblockInParentBlockInLocal() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(PUSH).ref("refs/heads/*").group(DEVS))
+        .update();
 
-    ProjectControl d = user(local, DEVS);
+    ProjectControl d = user(localKey, DEVS);
     assertCannotUpdate("refs/heads/master", d);
   }
 
   @Test
-  public void unblockForceEditTopicName() {
-    block(local, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+  public void unblockForceEditTopicName() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(block(EDIT_TOPIC_NAME).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .add(allow(EDIT_TOPIC_NAME).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     assertWithMessage("u can edit topic name")
         .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
         .isTrue();
   }
 
   @Test
-  public void unblockInLocalForceEditTopicName_Fails() {
-    block(parent, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
+  public void unblockInLocalForceEditTopicName_Fails() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(EDIT_TOPIC_NAME).ref("refs/heads/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(EDIT_TOPIC_NAME).ref("refs/heads/*").group(DEVS).force(true))
+        .update();
 
-    ProjectControl u = user(local, REGISTERED_USERS);
+    ProjectControl u = user(localKey, REGISTERED_USERS);
     assertWithMessage("u can't edit topic name")
         .that(u.controlForRef("refs/heads/master").canForceEditTopicName())
         .isFalse();
   }
 
   @Test
-  public void unblockRange() {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void unblockRange() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
   }
 
   @Test
-  public void unblockRangeOnMoreSpecificRef_Fails() {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
+  public void unblockRangeOnMoreSpecificRef_Fails() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/master").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
 
   @Test
-  public void unblockRangeOnLargerScope_Fails() {
-    block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/master");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void unblockRangeOnLargerScope_Fails() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(
+            blockLabel("Code-Review").ref("refs/heads/master").group(ANONYMOUS_USERS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
 
   @Test
-  public void nonconfiguredCannotVote() {
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void nonconfiguredCannotVote() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, REGISTERED_USERS);
+    ProjectControl u = user(localKey, REGISTERED_USERS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-1, range);
     assertCannotVote(1, range);
   }
 
   @Test
-  public void unblockInLocalRange_Fails() {
-    block(parent, LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+  public void unblockInLocalRange_Fails() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
 
   @Test
-  public void unblockRangeForChangeOwner() {
-    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+  public void unblockRangeForChangeOwner() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range =
         u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review", true);
     assertCanVote(-2, range);
@@ -846,65 +1057,97 @@
   }
 
   @Test
-  public void unblockRangeForNotChangeOwner() {
-    allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+  public void unblockRangeForNotChangeOwner() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
 
   @Test
-  public void blockChangeOwnerVote() {
-    block(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
+  public void blockChangeOwnerVote() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(CHANGE_OWNER).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCannotVote(-2, range);
     assertCannotVote(2, range);
   }
 
   @Test
-  public void unionOfPermissibleVotes() {
-    allow(local, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
+  public void unionOfPermissibleVotes() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
   }
 
   @Test
-  public void unionOfPermissibleVotesPermissionOrder() {
-    allow(local, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
-    allow(local, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
+  public void unionOfPermissibleVotesPermissionOrder() throws Exception {
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-2, range);
     assertCanVote(2, range);
   }
 
   @Test
-  public void unionOfBlockedVotes() {
-    allow(parent, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
-    block(parent, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
-    block(local, LABEL + "Code-Review", -2, +1, REGISTERED_USERS, "refs/heads/*");
+  public void unionOfBlockedVotes() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(DEVS).range(-1, +1))
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +2))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(blockLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, +1))
+        .update();
 
-    ProjectControl u = user(local, DEVS);
+    ProjectControl u = user(localKey, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertCanVote(-1, range);
     assertCannotVote(1, range);
   }
 
   @Test
-  public void blockOwner() {
-    block(parent, OWNER, ANONYMOUS_USERS, "refs/*");
-    allow(local, OWNER, DEVS, "refs/*");
+  public void blockOwner() throws Exception {
+    projectOperations
+        .project(parentKey)
+        .forUpdate()
+        .add(block(OWNER).ref("refs/*").group(ANONYMOUS_USERS))
+        .update();
+    projectOperations
+        .project(localKey)
+        .forUpdate()
+        .add(allow(OWNER).ref("refs/*").group(DEVS))
+        .update();
 
-    assertThat(user(local, DEVS).isOwner()).isFalse();
+    assertThat(user(localKey, DEVS).isOwner()).isFalse();
   }
 
   @Test
@@ -932,53 +1175,19 @@
     RefPattern.validate("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}");
   }
 
-  private InMemoryRepository add(ProjectConfig pc) {
-    List<CommentLinkInfo> commentLinks = null;
-
-    InMemoryRepository repo;
-    try {
-      repo = repoManager.createRepository(pc.getName());
-      if (pc.getProject() == null) {
-        pc.load(repo);
-      }
-    } catch (IOException | ConfigInvalidException e) {
-      throw new RuntimeException(e);
-    }
-    all.put(
-        pc.getName(),
-        new ProjectState(
-            projectCache,
-            allProjectsName,
-            allUsersName,
-            repoManager,
-            commentLinks,
-            capabilityCollectionFactory,
-            transferConfig,
-            metricMaker,
-            pc));
-    return repo;
+  private ProjectState getProjectState(Project.NameKey nameKey) throws Exception {
+    return projectCache.checkedGet(nameKey, true);
   }
 
-  private ProjectControl user(ProjectConfig local, AccountGroup.UUID... memberOf) {
-    return user(local, null, memberOf);
+  private ProjectControl user(Project.NameKey localKey, AccountGroup.UUID... memberOf)
+      throws Exception {
+    return user(localKey, null, memberOf);
   }
 
   private ProjectControl user(
-      ProjectConfig local, @Nullable String name, AccountGroup.UUID... memberOf) {
-    return new ProjectControl(
-        Collections.emptySet(),
-        Collections.emptySet(),
-        sectionSorter,
-        changeControlFactory,
-        permissionBackend,
-        refFilterFactory,
-        new MockUser(name, memberOf),
-        newProjectState(local));
-  }
-
-  private ProjectState newProjectState(ProjectConfig local) {
-    add(local);
-    return all.get(local.getProject().getNameKey());
+      Project.NameKey localKey, @Nullable String name, AccountGroup.UUID... memberOf)
+      throws Exception {
+    return projectControlFactory.create(new MockUser(name, memberOf), getProjectState(localKey));
   }
 
   private static class MockUser extends CurrentUser {
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 8f6119a..12c0838 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
@@ -32,7 +38,6 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.testing.Util;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
@@ -43,6 +48,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -57,10 +63,10 @@
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected AllProjectsName allProjects;
   @Inject private CommitsCollection commits;
-  @Inject private ProjectConfig.Factory projectConfigFactory;
+  @Inject private ProjectOperations projectOperations;
 
   private TestRepository<InMemoryRepository> repo;
-  private ProjectConfig project;
+  private Project.NameKey project;
 
   @Before
   public void setUp() throws Exception {
@@ -68,17 +74,22 @@
 
     Account.Id user = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
     testEnvironment.setApiUser(user);
+    project = projectOperations.newProject().create();
+    repo = new TestRepository<>(repoManager.openRepository(project));
+  }
 
-    Project.NameKey name = Project.nameKey("project");
-    InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
-    project = projectConfigFactory.create(name);
-    project.load(inMemoryRepo);
-    repo = new TestRepository<>(inMemoryRepo);
+  @After
+  public void tearDown() {
+    repo.getRepository().close();
   }
 
   @Test
   public void canReadCommitWhenAllRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
     ObjectId id = repo.branch("master").commit().create();
     ProjectState state = readProjectState();
     RevWalk rw = repo.getRevWalk();
@@ -89,8 +100,12 @@
 
   @Test
   public void canReadCommitIfTwoRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(allow(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
@@ -105,8 +120,12 @@
 
   @Test
   public void canReadCommitIfRefVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(deny(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     ObjectId id1 = repo.branch("branch1").commit().create();
     ObjectId id2 = repo.branch("branch2").commit().create();
@@ -121,8 +140,12 @@
 
   @Test
   public void canReadCommitIfReachableFromVisibleRef() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
-    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .add(deny(READ).ref("refs/heads/branch2").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     repo.branch("branch1").commit().parent(parent1).create();
@@ -139,7 +162,11 @@
 
   @Test
   public void cannotReadAfterRollbackWithRestrictedRead() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/heads/branch1").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
@@ -158,7 +185,11 @@
 
   @Test
   public void canReadAfterRollbackWithAllRefsVisible() throws Exception {
-    allow(project, READ, REGISTERED_USERS, "refs/*");
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
 
     RevCommit parent1 = repo.commit().create();
     ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
@@ -176,41 +207,19 @@
   }
 
   private ProjectState readProjectState() throws Exception {
-    return projectCache.get(project.getName());
-  }
-
-  protected void allow(ProjectConfig project, String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    Util.allow(project, permission, id, ref);
-    saveProjectConfig(project);
-  }
-
-  protected void deny(ProjectConfig project, String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    Util.deny(project, permission, id, ref);
-    saveProjectConfig(project);
-  }
-
-  protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(cfg.getName())) {
-      cfg.commit(md);
-    }
-    projectCache.evict(cfg.getProject());
+    return projectCache.get(project);
   }
 
   private void setUpPermissions() throws Exception {
-    ImmutableList<AccountGroup.UUID> admins = getAdmins();
-
     // Remove read permissions for all users besides admin, because by default
     // Anonymous user group has ALLOW READ permission in refs/*.
     // This method is idempotent, so is safe to call on every test setup.
-    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
-    for (AccessSection sec : pc.getAccessSections()) {
-      sec.removePermission(Permission.READ);
-    }
-    for (AccountGroup.UUID admin : admins) {
-      allow(pc, Permission.READ, admin, "refs/*");
-    }
+    TestProjectUpdate.Builder u = projectOperations.allProjectsForUpdate();
+    projectCache.checkedGet(allProjects).getConfig().getAccessSectionNames().stream()
+        .filter(sec -> sec.startsWith(R_REFS))
+        .forEach(sec -> u.remove(permissionKey(Permission.READ).ref(sec)));
+    getAdmins().forEach(admin -> u.add(allow(Permission.READ).ref("refs/*").group(admin)));
+    u.update();
   }
 
   private ImmutableList<AccountGroup.UUID> getAdmins() {
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 15c757d..75e1cd7 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -36,7 +36,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -340,11 +340,11 @@
     cfg.getLabelSections()
         .put(
             "My-Label",
-            Util.category(
+            TestLabels.label(
                 "My-Label",
-                Util.value(-1, "Negative"),
-                Util.value(0, "No score"),
-                Util.value(1, "Positive")));
+                TestLabels.value(-1, "Negative"),
+                TestLabels.value(0, "No score"),
+                TestLabels.value(1, "Positive")));
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 65c6e3f..3502853 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -18,13 +18,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.testing.Util.allow;
-import static com.google.gerrit.server.project.testing.Util.category;
-import static com.google.gerrit.server.project.testing.Util.value;
-import static com.google.gerrit.server.project.testing.Util.verified;
+import static com.google.gerrit.server.project.testing.TestLabels.label;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -42,9 +41,9 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
 import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -176,6 +175,9 @@
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
 
+  @Inject private ProjectConfig.Factory projectConfigFactory;
+  @Inject private ProjectOperations projectOperations;
+
   protected Injector injector;
   protected LifecycleManager lifecycle;
   protected Account.Id userId;
@@ -1049,19 +1051,23 @@
     TestRepository<Repo> repo = createProject("repo");
     Project.NameKey project =
         Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
 
     LabelType verified =
-        category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
-    cfg.getLabelSections().put(verified.getName(), verified);
-
-    String heads = RefNames.REFS_HEADS + "*";
-    allow(cfg, Permission.forLabel(verified().getName()), -1, 1, REGISTERED_USERS, heads);
-
+        label("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig cfg = projectConfigFactory.create(project);
+      cfg.load(md);
+      cfg.getLabelSections().put(verified.getName(), verified);
       cfg.commit(md);
     }
-    projectCache.evict(cfg.getProject());
+    projectCache.evict(project);
+
+    String heads = RefNames.REFS_HEADS + "*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1))
+        .update();
 
     ReviewInput reviewVerified = new ReviewInput().label("Verified", 1);
     ChangeInserter ins = newChange(repo, null, null, null, null, false);
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 7ca7ac3..a128593 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -9,6 +9,7 @@
     visibility = ["//visibility:public"],
     runtime_deps = ["//prolog:gerrit-prolog-common"],
     deps = [
+        "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
diff --git a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
index 655baa0..be0b8e7 100644
--- a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
+++ b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
@@ -19,7 +19,7 @@
 import static org.easymock.EasyMock.expect;
 
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.AbstractModule;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
@@ -58,7 +58,8 @@
 
   @Override
   protected void setUpEnvironment(PrologEnvironment env) throws Exception {
-    LabelTypes labelTypes = new LabelTypes(Arrays.asList(Util.codeReview(), Util.verified()));
+    LabelTypes labelTypes =
+        new LabelTypes(Arrays.asList(TestLabels.codeReview(), TestLabels.verified()));
     ChangeData cd = EasyMock.createMock(ChangeData.class);
     expect(cd.getLabelTypes()).andStubReturn(labelTypes);
     EasyMock.replay(cd);
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index 9d43f67..96bf84c 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.server.schema.NoteDbSchemaUpdater.requiredUpgrades;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
@@ -61,26 +61,20 @@
 
   @Test
   public void downgradeNotSupported() throws Exception {
-    try {
-      requiredUpgrades(14, versions(10, 11, 12, 13));
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .contains("Cannot downgrade NoteDb schema from version 14 to 13");
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> requiredUpgrades(14, versions(10, 11, 12, 13)));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("Cannot downgrade NoteDb schema from version 14 to 13");
   }
 
   @Test
   public void skipToFirstVersionNotSupported() throws Exception {
     ImmutableSortedSet<Integer> versions = versions(10, 11, 12);
     assertThat(requiredUpgrades(9, versions)).containsExactly(10, 11, 12).inOrder();
-    try {
-      requiredUpgrades(8, versions);
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10");
-    }
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> requiredUpgrades(8, versions));
+    assertThat(thrown).hasMessageThat().contains("Cannot skip NoteDb schema from version 8 to 10");
   }
 
   private static class TestUpdate {
@@ -230,12 +224,8 @@
             seedGroupSequenceRef();
           }
         };
-    try {
-      u.update();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("NoteDb change migration was not completed");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> u.update());
+    assertThat(thrown).hasMessageThat().contains("NoteDb change migration was not completed");
     assertThat(u.getMessages()).isEmpty();
     assertThat(u.readVersion()).isEmpty();
   }
@@ -249,12 +239,8 @@
             setNotesMigrationConfig();
           }
         };
-    try {
-      u.update();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e).hasMessageThat().contains("upgrade to 2.16.x first");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> u.update());
+    assertThat(thrown).hasMessageThat().contains("upgrade to 2.16.x first");
     assertThat(u.getMessages()).isEmpty();
     assertThat(u.readVersion()).isEmpty();
   }
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
index 464a452..8ccf631 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_VERSION;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -54,14 +54,10 @@
   public void readInvalid() throws Exception {
     ObjectId blobId = tr.blob(" 1 2 3 ");
     tr.update(REFS_VERSION, blobId);
-    try {
-      manager.read();
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("invalid value in refs/meta/version blob at " + blobId.name());
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> manager.read());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("invalid value in refs/meta/version blob at " + blobId.name());
   }
 
   @Test
@@ -80,13 +76,9 @@
   @Test
   public void incrementWrongOldVersion() throws Exception {
     tr.update(REFS_VERSION, tr.blob("123"));
-    try {
-      manager.increment(456);
-      assert_().fail("expected StorageException");
-    } catch (StorageException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .isEqualTo("Expected old version 456 for refs/meta/version, found 123");
-    }
+    StorageException thrown = assertThrows(StorageException.class, () -> manager.increment(456));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Expected old version 456 for refs/meta/version, found 123");
   }
 }
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 79faf60..1d84d67 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
@@ -106,12 +106,11 @@
     ObjectId oldMetaId = getMetaId(id);
     try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
       bu.addOp(id, new AddMessageOp("Excessive update"));
-      try {
-        bu.execute();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        assertThat(e).hasMessageThat().isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
     }
     assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES);
     assertThat(getMetaId(id)).isEqualTo(oldMetaId);
@@ -125,12 +124,11 @@
     try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
       bu.addOp(id, new AddMessageOp("Update on PS1", PatchSet.id(id, 1)));
       bu.addOp(id, new AddMessageOp("Update on PS2", PatchSet.id(id, 2)));
-      try {
-        bu.execute();
-        assert_().fail("expected ResourceConflictException");
-      } catch (ResourceConflictException e) {
-        assertThat(e).hasMessageThat().isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
-      }
+      ResourceConflictException thrown =
+          assertThrows(ResourceConflictException.class, () -> bu.execute());
+      assertThat(thrown)
+          .hasMessageThat()
+          .isEqualTo(TooManyUpdatesException.message(id, MAX_UPDATES));
     }
     assertThat(getUpdateCount(id)).isEqualTo(MAX_UPDATES - 1);
     assertThat(getMetaId(id)).isEqualTo(oldMetaId);
diff --git a/javatests/com/google/gerrit/server/util/LabelVoteTest.java b/javatests/com/google/gerrit/server/util/LabelVoteTest.java
index 9069928..bda99a8 100644
--- a/javatests/com/google/gerrit/server/util/LabelVoteTest.java
+++ b/javatests/com/google/gerrit/server/util/LabelVoteTest.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.util;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.server.util.LabelVote.parse;
 import static com.google.gerrit.server.util.LabelVote.parseWithEquals;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.junit.Test;
 
@@ -82,11 +82,6 @@
   }
 
   private void assertParseWithEqualsFails(String value) {
-    try {
-      parseWithEquals(value);
-      assert_().fail("expected IllegalArgumentException when parsing \"%s\"", value);
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
+    assertThrows(IllegalArgumentException.class, () -> parseWithEquals(value));
   }
 }
diff --git a/lib/errorprone/BUILD b/lib/errorprone/BUILD
new file mode 100644
index 0000000..b5c130b
--- /dev/null
+++ b/lib/errorprone/BUILD
@@ -0,0 +1,6 @@
+java_library(
+    name = "annotations",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@error-prone-annotations//jar"],
+)
diff --git a/plugins/delete-project b/plugins/delete-project
index a4b777a..6e94a1f 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit a4b777a173feb2cfaaad591e2fded37f15500e2b
+Subproject commit 6e94a1f4f09c5e0aef86bafd80be35fa8b2842e6
diff --git a/plugins/hooks b/plugins/hooks
index 60fb334..1244739 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 60fb334f44329caca37d8aa0d43feba651c959b2
+Subproject commit 12447394c90141595ab630129e24ab66d2f39a17
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 9edc195..a51055a 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 9edc1950d3c0a3717cefda1688a4c37e7fc652fb
+Subproject commit a51055a8f3b71f2ccf634016c42eb5b8086a373b
diff --git a/plugins/replication b/plugins/replication
index aa07963..b4b90fd 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit aa07963def69e4423444a078a662072e09629cec
+Subproject commit b4b90fd2cc2b53789bffedc5ae829bd2d9b94009
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 2091fdf..70f1f3a 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 2091fdfc99f2cec60ae3fda2538ae1993b0fc8b8
+Subproject commit 70f1f3a860fe9e276a3ed216bf2b179ea0e8cde3
diff --git a/plugins/webhooks b/plugins/webhooks
index 1c860ae..8078749 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 1c860ae557f3851b2d98978bafba644de5e6c0b8
+Subproject commit 8078749bfd64d9dcd3372bc3f8965d192747c5b4
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 7831cfa..6d30a14 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -191,7 +191,7 @@
         "embed/test.html",
         "test/common-test-setup.html",
         ":embed_test_files",
-        ":polygerrit_embed_ui.zip",
+        ":pg_code.zip",
         ":test_components.zip",
     ],
     # Should not run sandboxed.
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index fec459b..970bfc7 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>async-foreach-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="async-foreach-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index c21e96f..8a76d9d 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>base-url-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <script>
   /** @type {string} */
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index 96d4a08..4f16e79 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>docs-url-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="docs-url-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
index e445a78..15affee 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>dom-util-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="dom-util-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
index 0b37a0d..817d26e 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-access-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index 7c23c00..60817aa 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-admin-nav-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
index 820d6bc..64f0b3a 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-anonymous-name-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-anonymous-name-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index b052d06..9b7339d 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-table-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
index f6c765f..9973ae8 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-list-view-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index b858c51..3db4084 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>gr-patch-set-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="gr-patch-set-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 75c2433..0046290 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -15,10 +15,12 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <title>gr-path-list-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <link rel="import" href="gr-path-list-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
index 07d3484..0e2e99f 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
 <script src="../../scripts/rootElement.js"></script>
 
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index 943e000..2c4b376 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>tooltip-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
index d909e86..8601397 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>gr-url-encoding-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="gr-url-encoding-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 4266b22..51adf2e 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -96,8 +96,8 @@
 NOTE: doc-only shortcuts will not be customizable in the same way that other
 shortcuts are.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
 
 <script>
 (function(window) {
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index d013299..9d5481d 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="keyboard-shortcut-behavior.html">
 
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index 2cb00f4..88aea2d 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../base-url-behavior/base-url-behavior.html">
 <script>
 (function(window) {
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 6af43dc..013ec2e 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <script>
   /** @type {string} */
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
index 68000bc..43022d9 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
@@ -23,7 +23,7 @@
   /** @polymerBehavior Gerrit.SafeTypes */
   Gerrit.SafeTypes = {};
 
-  const SAFE_URL_PATTERN = /^(https?:\/\/|mailto:|\/|#)/i;
+  const SAFE_URL_PATTERN = /^(https?:\/\/|mailto:|[^:/?#]*(?:[/?#]|$))/i;
 
   /**
    * Wraps a string to be used as a URL. An error is thrown if the string cannot
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
index 6e040a3..5d949a5 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -17,9 +17,11 @@
 -->
 
 <title>safe-types-behavior</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
 <link rel="import" href="safe-types-behavior.html">
 
@@ -119,4 +121,4 @@
       });
     });
   });
-</script>
\ No newline at end of file
+</script>
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 45bc5f6..398ed9c 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
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index 21a426f..6b08ed3 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-access-section</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-access-section.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index ea08d89..c448f5b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index 065a757..58c7be4 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-group-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index b6a6d27..f70025d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 56079e3..178d056 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-admin-view.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
index 3873083..b7932dd 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
index d735acb..cf9e0fa 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-delete-item-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-delete-item-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index da1c871..252dcfd 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index aa4da68..3a3683f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-change-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-change-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
index d96a935..73a3f4e 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
index 95ffdb1..300440e 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-group-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-group-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
index 0557021..a1fbd37 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
index 39e200a..77cce5a 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-pointer-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-pointer-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
index b38fab5..0feeced 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
index 79079f5..7e32c5c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-repo-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-repo-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
index 05b176c..071beda 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
index 59a665b..313d465 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group-audit-log</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-audit-log.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index bcfc9e1..787f2b4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -17,9 +17,9 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index b86d971..15a59c8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group-members</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-members.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index b92dc4b..9f49e4b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index 226f3ab..1672e85 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-group</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index a5bb5fd..78c30ff 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index 5f1f9b5..8e57534 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-permission</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-permission.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
index ca98c50..2625d65 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
index dc3f67e..39e4ddc 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-config-array-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-config-array-editor.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
index 9e4396a..26f37e1 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
index 9781cf7..96fff60 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-list.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index b6e56de..40d32d4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index 20e2b8e..abd11d6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-access</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-access.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
index 7db4e4c..49ff186 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
index 9f9ac92..49d8765 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-command</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-command.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
index dba01aa..ea4d165 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index 76c65e8..2976923 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-commands</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-commands.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
index 1d49db9..6cc0958 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
index 94bf5e0..4f76983 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-dashboards</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-dashboards.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
index fccfa6a..44b94e1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
@@ -17,8 +17,8 @@
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index 5d2a9ad..d8d4f7c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-detail-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-detail-list.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
index 0490db2..1a728ab 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
@@ -14,10 +14,10 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
index 4bc023f..c77592c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-list.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
index 7f2cbe7..69c86d9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 
 <link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
index 37f43f4..07da7c7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-plugin-config</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-plugin-config.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index cd27322..f6b6d29 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index c987278..8c77ae0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo.html">
 
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index c8ae650..7ba541f 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index f85c2b2..17e8c6c 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-rule-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-rule-editor.html">
 
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 02a2a08..eaba285 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
@@ -19,7 +19,7 @@
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
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 3637653..df4a442 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
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list-item</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></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 48d5075..1ca5668 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
@@ -17,7 +17,7 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.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">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 3911364..2367aac 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-list-view.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index ef17baa..6167630 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -20,7 +20,7 @@
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
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 d5b9aa9..dce2a55 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
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-change-list.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
index ecbd67e..11493f4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
index 09d95fd..c43d62a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-change-help</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
index e6d123c..4cef6f7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
index e00037d..89ad573 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-create-commands-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-create-commands-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
index d12d84b..def5228 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-repo-branch-picker/gr-repo-branch-picker.html">
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 b0ba8a2b..4360d5d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 618ec65..f9eb256 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dashboard-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dashboard-view.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
index 2394e24..d445185 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
 <link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
index 2328725..0b4459c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/dashboard-header-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
index a561e09..49af1b4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-header.html">
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
index 89e2b7d..49faad5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index c33be3b..e837a5b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-user-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-user-header.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
index 582c83b..95ef881 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
index 20d127d..57bdd1d 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-entry</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
index 1bfc5eb..9cb936a 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../gr-account-entry/gr-account-entry.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
index 544238b..d32c269 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 278875e..bcb0df4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../admin/gr-create-change-dialog/gr-create-change-dialog.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 1d83819..184e175 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-actions</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 5b36221..b2e8030 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-metadata</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
 <link rel="import" href="gr-change-metadata.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index c07a775..4d4ba3a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-voting-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index b23ac8d..e27ca1a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-metadata</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../core/gr-router/gr-router.html">
 <link rel="import" href="gr-change-metadata.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
index e79bce1..80fc1ae 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index 3f35158..2ceac39 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-requirements</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-requirements.html">
 
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 e8158b8..344c8dd 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
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
+<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 89d799b..e8de93b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="../../edit/gr-edit-constants.html">
 <link rel="import" href="gr-change-view.html">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index e4183df..f1d677e 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -18,7 +18,7 @@
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 9996abc..c18ae8d 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-comment-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
index b6c8fcc..902bf41 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index 438a3f3..d25a871 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-commit-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../core/gr-router/gr-router.html">
 <link rel="import" href="gr-commit-info.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
index 8803eb3..145d126 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 95d5374..3a3cad6 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-abandon-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-abandon-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
index cd196ec..55b14b1 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
index 77b102c..dbf332c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-cherrypick-conflict-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
index 84558ac..508e881 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
index 5c51fe0..22a2aba 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-cherrypick-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-cherrypick-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
index 621ef0a..c07bb37 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
index e619425..8d6e029 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-move-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-move-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 912bbfa6a..116b26f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index c6e9ec4..cd5b130 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-rebase-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-rebase-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 9e5f1de..c68912c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index c5a1bde..6e41555 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-revert-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-confirm-revert-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
index 1036b7f..42ecacf 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
index 86c15f6..40fa29a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-confirm-submit-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-confirm-submit-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 28d25d2..ab7525f 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index ee284b9..282d8de 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-download-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-download-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index f7d90eb..bdb4295 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index adfeeb4..ac626ab 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-file-list-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-file-list-header.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 29a12df..f087b21 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
 <link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index c69c146..9529d38 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-file-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
index b824f1c..a749431 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index 539011a..68c77e6 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-included-in-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-included-in-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 27c5baa..571ddf9 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../../styles/gr-voting-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 1e4d471..4920e20 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-score-row</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-score-row.html">
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
index bb7c7d8..c607a9f 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-label-score-row/gr-label-score-row.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
index a2629b6..f986a58 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-scores</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-scores.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 7b49d2a..df3cc37 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 <link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 64a5b26..ef5a756 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-message</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-message.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 80708a1..7545dd8 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-message/gr-message.html">
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index b75b93a..d6b887f 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-messages-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 30ebc08..4a70506 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 48cc565..06b7a5d 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-related-changes-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-related-changes-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 7101249..dd9e397 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reply-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 45b9c15..32bcc3f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -15,12 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index aec3491..b99c3f0 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reply-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reply-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 73e8bea..72c5b44 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 1a406c9..80359e0 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reviewer-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index 4d8e5ae..50782be 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 792644e..bd4a6ac 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-thread-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-thread-list.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
index 792c300..ff4ab39 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
index a5a6e76..66d0a01 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-upload-help-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-upload-help-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 2c2fb4e..7949002 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index fe63a3e..37d8882 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-dropdown.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
index f8bf33c..09b928e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
index e2c314b..648f8be 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-error-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index 4ca106e..db40496 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-alert/gr-alert.html">
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 88e3efd..9140c17 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-error-manager</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-error-manager.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
index 2ff7953..276db72 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-key-binding-display">
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
index 0361d76..39c8af8 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-key-binding-display.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index e3552cc..e153074 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
index 50579dd..1a3d6c7 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-key-binding-display</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-keyboard-shortcuts-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index c2b28d6..0213f1a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 03586ea..782a8da 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-main-header</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-main-header.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
index 2f72338..73ce86a 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-navigation</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
index 6faeec1..825a5fc 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-jank-detector</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <script src="gr-jank-detector.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index 935de6b..6588df4 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-reporting">
   <script src="gr-jank-detector.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 2087790..f7d2f62 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reporting</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-reporting.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 68ddef6..6035069 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
@@ -28,6 +28,6 @@
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
   </template>
-  <script src="../../../bower_components/page/page.js"></script>
+  <script src="/bower_components/page/page.js"></script>
   <script src="gr-router.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 4dbcd44..fe28f81 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-router</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-router.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 3a48213..d766f74 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -17,7 +17,7 @@
 
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 93e0e307..1365c00 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-search-bar</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="gr-search-bar.html">
 <script src="../../../scripts/util.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
index 4c98068..06e354c 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
index af0fc3c..a70eb7c 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-smart-search</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-smart-search.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
index c31bd1166..317e9e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 1e53a14..c44e8c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="./gr-comment-api.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
index 56a6fb9..d743d92 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-coverage-layer">
   <template>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
index edd88a2..45a67e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-coverage-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script src="../gr-diff/gr-diff-line.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 42fb567..fc945cd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index fa54756..b4a91b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-builder</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 <script src="../gr-diff/gr-diff-line.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index c24574e..99d0498 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 
 <dom-module id="gr-diff-cursor">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index f111378..d36a72d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-cursor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
index 652aa4c..86d5e45 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="gr-annotation.js"></script>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index c912a16..612aab6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 191d1d2..b83dd53 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-highlight</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-highlight.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
index 05f48d8..fc71be6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index d27a0e5..6d4e408 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-diff-host.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
index 8251e53..ad7ff11 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
index c011106..adeaa15 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-mode-selector</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-diff-mode-selector.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
index ae53f76..10d2828 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
index 663cf25..922ac87 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-diff-processor">
   <script src="../gr-diff/gr-diff-line.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 0d27b9e..83e4680 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -239,19 +239,22 @@
     },
 
     _linesFromSection(section, offsetLeft, offsetRight) {
-      const lines = [];
       if (section.ab) {
-        lines.push(...section.ab.map((row, i) =>
-          this._lineFromRow(
-              GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i)));
+        return section.ab.map((row, i) => this._lineFromRow(
+            GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
       }
+      let lines = [];
       if (section.a) {
-        lines.push(...this._deltaLinesFromRows(
+        // Avoiding a.push(...b) because that causes callstack overflows for
+        // large b, which can occur when large files are added removed.
+        lines = lines.concat(this._deltaLinesFromRows(
             GrDiffLine.Type.REMOVE, section.a, offsetLeft,
             section[DiffHighlights.REMOVED]));
       }
       if (section.b) {
-        lines.push(...this._deltaLinesFromRows(
+        // Avoiding a.push(...b) because that causes callstack overflows for
+        // large b, which can occur when large files are added removed.
+        lines = lines.concat(this._deltaLinesFromRows(
             GrDiffLine.Type.ADD, section.b, offsetRight,
             section[DiffHighlights.ADDED]));
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 0e57dbf..23dc12d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-processor test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-processor.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
index f9822f2..4679b7d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 469a894..0f5c6dd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-selection</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-selection.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 57525e1..d9cf979 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -15,12 +15,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index c4d6c95..1fde41c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-diff-view.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index 9dc5311..11a370a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -18,8 +18,10 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-group</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="gr-diff-line.js"></script>
 <script src="gr-diff-group.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 72fc1ee..287e983 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 762028a..9641a1e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index 3de4284..114d840 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 99d22c6..922a11c 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-patch-range-select</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
index c9e9f50..17a4866 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-ranged-comment-layer">
   <template>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 682c026..b057d69 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-ranged-comment-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../gr-diff/gr-diff-line.js"></script>
 
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 633530f..c2983f7 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index dece366..b950e7b 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-selection-action-box</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-selection-action-box.html">
 
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
index 67c32bb..dd6bfec 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html">
 
 <dom-module id="gr-syntax-layer">
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index b63675a..472db21 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-syntax-layer</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="gr-syntax-layer.html">
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
index 720f353..616172b 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
@@ -14,11 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
index 84addb0..84298e2 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-documentation-search</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-documentation-search.html">
 
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
index 093e979..5c13ff9 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-default-editor">
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
index 423c493..c986e7c 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-default-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-default-editor.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 81b3c07..ea5024a 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index c67a2af..75a371f8 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-controls</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-edit-controls.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
index c57a147..1ab08a3 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 12d9e0b..7979e57 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-file-controls</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="../gr-edit-constants.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index f80e9f8..6c2cbb5 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 63f4314..abb8131 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editor-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-editor-view.html">
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/gr-app-it_test.html
index 2601aeb..cc8b784 100644
--- a/polygerrit-ui/app/elements/gr-app-it_test.html
+++ b/polygerrit-ui/app/elements/gr-app-it_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-app-it_test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
 <link rel="import" href="gr-app.html">
 
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index ac00fcf..227616b 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -34,9 +34,9 @@
   window.Gerrit = Gerrit;
 </script>
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html">
-<link rel="import" href="../bower_components/polymer/lib/legacy/legacy-data-mixin.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html">
+<link rel="import" href="/bower_components/polymer/lib/legacy/legacy-data-mixin.html">
 <link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
@@ -45,7 +45,7 @@
     safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
   });
 </script>
-<script src="../bower_components/moment/moment.js"></script>
+<script src="/bower_components/moment/moment.js"></script>
 
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 71ceab4..19d06a9 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-app</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../test/common-test-setup.html"/>
 <link rel="import" href="gr-app.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index 966efac..6883e7e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-admin-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-admin-api.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
index 208f1e8..ece8677 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-attribute-helper">
   <script src="gr-attribute-helper.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index 86238cf..efd6104 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-attribute-helper</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-attribute-helper.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
index eddb52b..dd532e1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-change-metadata-api">
   <script src="gr-change-metadata-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
index 252e812..2cdae3c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-dom-hooks">
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index 3dde458..1e946c3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dom-hooks</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dom-hooks.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
index 50b80d5..53ff901 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-endpoint-decorator">
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 31d3150..65a5f08 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-endpoint-decorator</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-endpoint-decorator.html">
 <link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
index 9d28ac3..6a5b558 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-endpoint-param">
   <script src="gr-endpoint-param.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
index d34bdef..717d52b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-event-helper">
   <script src="gr-event-helper.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
index 47274f6..08c1df9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-event-helper</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-event-helper.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
index a83b2ab..6a55349 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-external-style">
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index ec2888d..9566067 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-external-style</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-external-style.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
index 8e106cc..f277899 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 9901d9f..e577182 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-host</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-host.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
index ce0bf1b..402d988 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 
 <dom-module id="gr-plugin-popup">
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
index 91386b9..1f1e81e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-popup</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-popup.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
index 2fdf28c..26ece30 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-popup.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index 983c795..53370e2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-popup-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-popup-interface.html"/>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
index c9486ae..2fd4534 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../admin/gr-repo-command/gr-repo-command.html">
 
 <dom-module id="gr-plugin-repo-command">
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
index 34c9797..8e6c053 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-repo-command.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index bb9ae87..7c7564b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-repo-api.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
index 7c916dc..20cc71b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-item.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
index cabd26b..d34ca94 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-settings-api.html">
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
index 496d0e7..97966ed 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-custom-plugin-header">
   <template>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
index b84f5b9..d6e67fe 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-custom-plugin-header.html">
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 8d23ea2..82eb0f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-theme-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="gr-theme-api.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index f534771..7d469a3 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index f91277a..de222a9 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-info.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
index 72ea503..852161c 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
index 56122a9..e0a3afa 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-agreements-list.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index 4f69513..43bb2db 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 32fab9d..e1ec32c 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-table-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
index d5f1dc3..bb5c069 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
@@ -16,8 +16,8 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
index 2304d15..53d6be1 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-cla-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-cla-view.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
index b3e6990..33fc879 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index 42171b7..c1c5c52 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-edit-preferences</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-edit-preferences.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index 0a7433e..98b9b6d 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index e937f8b..e8ecd7a 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-email-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-email-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
index 7a63605..c21e84d 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index 0a2ae78..c3cd8e1 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-gpg-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-gpg-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
index 2c7afd3..ca500c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 3fa5a36..ac17521 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-group-list.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
index 2fe07ca..8e5db7d 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index ca50b2b..8924058 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-http-password.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
index 872e558..be851b1 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
index c77a1b9..19c9df3 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-identities</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-identities.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
index aa42623..56f4cfa 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index c8a54b6..917026a 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-menu-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 5f1794c..66b06f8 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index 93a3188..d1b5c80 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-registration-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-registration-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
index 30a3801..704a8698d 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-settings-item">
   <template>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
index f64d898..846f776 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
 
 <dom-module id="gr-settings-menu-item">
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 538bab7..68518a0 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-menu-page-styles.html">
 <link rel="import" href="../../../styles/gr-page-nav-styles.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 506c6af..6dcf124 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-settings-view.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
index ab12403..784e0fc 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 1785d1f..991f17c 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-ssh-editor</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-ssh-editor.html">
 
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 85fe368..e314608 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 9022bcc..4193382 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-settings-view</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-watched-projects-editor.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index 543ed85..6de10a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-account-link/gr-account-link.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-icons/gr-icons.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index bdf37bf..fb760d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -17,7 +17,7 @@
 
 <link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-avatar/gr-avatar.html">
 <link rel="import" href="../gr-limited-text/gr-limited-text.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index 288a670..d3f29dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 34b0de6..d3575b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -16,7 +16,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../gr-account-label/gr-account-label.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index 6d1831e..134c579 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-account-link</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-account-link.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index b00fded..2b043c4 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
index 095e640..2338d55 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-alert</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-alert.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index 7b82635..383d129 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -15,10 +15,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <script src="../../../scripts/rootElement.js"></script>
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index d4d54ff..07a3762 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-autocomplete-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-autocomplete-dropdown.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index a878174..ac739c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/paper-input/paper-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 1a76f98..217321f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-autocomplete.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index bc63acf..5c9b66e 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 5ce17c0..42d7678 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-avatar</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-avatar.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index cdf617f..754c3c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
+<link rel="import" href="/bower_components/paper-button/paper-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-button">
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index ed0da2e..807d095 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-button</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-button.html">
 
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 a14c652..ebe3a6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
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 0ca9368..7ee22a7 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
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-star</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-star.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
index 99ddff1..fa95382 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index f73fc02..3ac4016 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-status</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-change-status.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
index 8c80b37..bee134b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index 2e1b3bd..86da001 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment-thread</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index a470285..1460035 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -15,9 +15,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 7ca5242..c829343 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/page/page.js"></script>
+<script src="/bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-comment.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
index 9decfa9..bf20429 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
index 32ca557..f6b4a27 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
index d6e9dca..c092b7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-copy-clipboard</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-copy-clipboard.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
index e4d896b..d061ac2 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-count-string-formatter</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-count-string-formatter.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
index d619b18..94d7aaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-cursor-manager">
   <template></template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index adbe618..0793ccd 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-cursor-manager</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-cursor-manager.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 481dd2f..f3ea177 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index ad4d0da..a0bf207 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-date-formatter</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
index 797c8ea..0321e58 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
index 4a5a181..1456e77 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dialog</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dialog.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
index 1c9f469..e21e5a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
index 5bd72c4..f7b10e0 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-diff-preferences</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-diff-preferences.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
index 6aec5a6..d4a2ab2 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
+<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
 <link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index c59e56a..3b7e8f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-download-commands</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-download-commands.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index f4b120a..1d608c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -14,11 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../../bower_components/paper-item/paper-item.html">
-<link rel="import" href="../../../bower_components/paper-listbox/paper-listbox.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/paper-item/paper-item.html">
+<link rel="import" href="/bower_components/paper-listbox/paper-listbox.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
index 87fd8de..5f07fc9 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dropdown-list</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dropdown-list.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 47ab03a..22dcbf0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -17,8 +17,8 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 7bb4dce..bf1c9fa 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-dropdown</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-dropdown.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
index 6cd87f5..aa102e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../gr-storage/gr-storage.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index cc44d9b..3f5ccb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-content</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-editable-content.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index ddc35bf..5faedd2 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -14,11 +14,11 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
+<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/paper-input/paper-input.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-editable-label">
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 6815173..5dad9a6 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-editable-label.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
index 674ff97..47acd0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-fixed-panel">
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index 9eac7f7..75e9901 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-fixed-panel</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-fixed-panel.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
index 3995595..b6ad1af 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-linked-text/gr-linked-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index ad036c5..801190a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-editable-label</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-formatted-text.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
index 7e3246f..bb6fbb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -46,4 +46,4 @@
   </template>
   <script src="../../../scripts/rootElement.js"></script>
   <script src="gr-hovercard.js"></script>
-</dom-module>
\ No newline at end of file
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index e3e252f..aa13407 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-hovercard</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-hovercard.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 1f885af..7bd6f48 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../bower_components/iron-iconset-svg/iron-iconset-svg.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/iron-iconset-svg/iron-iconset-svg.html">
 
 <iron-iconset-svg name="gr-icons" size="24">
   <svg>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
index 03c8c5e..653999c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation-actions-context</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script src="../../diff/gr-diff-highlight/gr-annotation.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index bf7c2cb..78f9650 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-annotation-actions-js-api-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index fef4fc9..30bd366 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-actions-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <!--
 This must refer to the element this interface is wrapping around. Otherwise
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 278f95a..842a2fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-change-reply-js-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <!--
 This must refer to the element this interface is wrapping around. Otherwise
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index d8a662e..e04867a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 113a6f7..055fc3fcb 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index ca87956..cff1a4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-action-context</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index b00b5ac..8ed7f14 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-endpoints</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index 5983621..8626280 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-plugin-rest-api</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-js-api-interface.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
index ca5c49f..e3cf34b 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../styles/gr-voting-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index 8bc358d..96d9fd7 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -17,9 +17,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-label-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-label-info.html">
 
@@ -227,4 +229,4 @@
       assert.isTrue(isHidden(element.$$('.placeholder')));
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html b/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
index fe290b7..55ecc98 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <dom-module id="gr-label">
   <template strip-whitespace>
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
index c001ce7..986bce1 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
index 6bcaa18..bcd060b 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-labeled-autocomplete</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-labeled-autocomplete.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
index 4137485..fb55c67 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-lib-loader">
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index cf9a41c..10d1608 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-lib-loader</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-lib-loader.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
index 91866e5..d00416b 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 
 <dom-module id="gr-limited-text">
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
index 16eb960..b07971b 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-limited-text</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <link rel="import" href="gr-limited-text.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index fab562a..a3dd054 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../gr-button/gr-button.html">
 <link rel="import" href="../gr-icons/gr-icons.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
index eb57428..22a2eaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -18,11 +18,13 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-linked-chip</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
 
 <link rel="import" href="gr-linked-chip.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
index c35768f..5697e77 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-<script src="../../../bower_components/ba-linkify/ba-linkify.js"></script>
+<script src="/bower_components/ba-linkify/ba-linkify.js"></script>
 <script src="link-text-parser.js"></script>
 <dom-module id="gr-linked-text">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 23c1442..73295b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-linked-text</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
index be02d40..1ee168d 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
index 09e68dd..c67d8b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-list-view</title>
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-list-view.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
index e94b655..82c1169 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -15,8 +15,8 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-overlay">
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
index ee05b69..08b7497 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-overlay</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
index 3885497..f1c3a6f 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-page-nav">
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
index 428bab3..b384b47 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-page-nav</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/page/page.js"></script>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/page/page.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
index d794dd6..416815b 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
@@ -14,8 +14,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
index 989e838..1ed9151 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-repo-branch-picker</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-repo-branch-picker.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index a571be9..cfdc6ee 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-auth</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
index c5a0dfe..d3500d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-etag-decorator">
   <script src="gr-etag-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 09ae1da..76c8c2c 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-etag-decorator</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 
 <script src="gr-etag-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 562980c..607802f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
@@ -23,8 +23,8 @@
 <link rel="import" href="gr-etag-decorator.html">
 
 <!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
-<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
-<script src="../../../bower_components/fetch/fetch.js"></script>
+<script src="/bower_components/es6-promise/dist/es6-promise.min.js"></script>
+<script src="/bower_components/fetch/fetch.js"></script>
 
 <dom-module id="gr-rest-api-interface">
   <!-- NB: Order is important, because of namespaced classes. -->
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index ef4e401..d00e00d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-rest-api-interface</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 202c52a..fdf79af 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-reviewer-updates-parser</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <script src="../../../scripts/util.js"></script>
 <script src="gr-reviewer-updates-parser.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index 05c2cee..9a24ddc 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <dom-module id="mock-diff-response">
   <template></template>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
index e73d41c..02afb38 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-select">
   <slot></slot>
   <script src="gr-select.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index 1748ec06..66ebb79 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-select</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-select.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
index fe6ed88..dbbf98b 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
index a49f76f..3f2f8ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-shell-command</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-shell-command.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
index 6fc2f3f..7215b26 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
@@ -14,7 +14,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <dom-module id="gr-storage">
   <script src="gr-storage.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index 6b89af2..0482584 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-storage.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index 10c9111..fc532af 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -14,14 +14,14 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-textarea">
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 3a52543..8b6eff2 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-textarea</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-textarea.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
index 65f1fda..b4fefe1 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 
 <dom-module id="gr-tooltip-content">
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 438d436..f9350c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip-content.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
index 9947d61..36378f6 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-tooltip">
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
index 3a47288..f59f6e1 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -17,9 +17,11 @@
 -->
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-storage</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-tooltip.html">
 
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
index 433872d..7e5810b 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -18,9 +18,11 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>revision-info</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="revision-info.html">
 
diff --git a/polygerrit-ui/app/embed/embed.html b/polygerrit-ui/app/embed/embed.html
index 1b2f20f..783589d 100644
--- a/polygerrit-ui/app/embed/embed.html
+++ b/polygerrit-ui/app/embed/embed.html
@@ -20,7 +20,7 @@
   let Gerrit = window.Gerrit || {};
   window.Gerrit = Gerrit;
 </script>
-<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../elements/change/gr-change-view/gr-change-view.html">
 <link rel="import" href="../elements/core/gr-search-bar/gr-search-bar.html">
 <link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html
index 7ca75c9..1e3f5d7 100644
--- a/polygerrit-ui/app/embed/embed_test.html
+++ b/polygerrit-ui/app/embed/embed_test.html
@@ -18,10 +18,12 @@
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>embed_test</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../polygerrit_ui/elements/embed.html"/>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="embed.html"/>
 
 <script>void(0);</script>
 
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html
index eed2fef..955eaee 100644
--- a/polygerrit-ui/app/embed/test.html
+++ b/polygerrit-ui/app/embed/test.html
@@ -19,8 +19,8 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>Embed Test Runner</title>
 <meta charset="utf-8">
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script>
-  WCT.loadSuites(['embed_test.html']);
+  WCT.loadSuites(['../embed/embed_test.html']);
 </script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh
index bb08bb0..0d8f58f 100755
--- a/polygerrit-ui/app/embed_test.sh
+++ b/polygerrit-ui/app/embed_test.sh
@@ -4,15 +4,15 @@
 
 t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX)
 components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip
-code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/polygerrit_embed_ui.zip
-index=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html
-tests=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/*_test.html
+code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/pg_code.zip
 
+echo $t
 unzip -qd $t $components
 unzip -qd $t $code
+# Purge test/ directory contents coming from pg_code.zip.
+rm -rf $t/test
 mkdir -p $t/test
-cp $index $t/test/
-cp $tests $t/test/
+cp $TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html $t/test/
 
 if [ "${WCT_HEADLESS_MODE:-0}" != "0" ]; then
     CHROME_OPTIONS=[\'start-maximized\',\'headless\',\'disable-gpu\',\'no-sandbox\']
@@ -61,9 +61,9 @@
     };
 EOF
 
-export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+export PATH="$(dirname $NPM):$PATH"
 
 cd $t
 test -n "${WCT}"
 
-$(basename ${WCT}) ${WCT_ARGS}
+${WCT} ${WCT_ARGS}
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index c5979fa..696f6a5 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -17,7 +17,8 @@
 -->
 
 <link rel="import"
-    href="../bower_components/polymer-resin/standalone/polymer-resin.html" />
+    href="/bower_components/polymer-resin/standalone/polymer-resin.html" />
+<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
   security.polymer_resin.install({
     allowedIdentifierPrefixes: [''],
@@ -32,6 +33,7 @@
             + JSON.stringify(args));
       }
     },
+    safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
   });
 </script>
 <script>
@@ -58,6 +60,6 @@
   })();
 </script>
 <link rel="import"
-    href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
+    href="/bower_components/iron-test-helpers/iron-test-helpers.html" />
 <link rel="import" href="test-router.html" />
-<script src="../bower_components/moment/moment.js"></script>
+<script src="/bower_components/moment/moment.js"></script>
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
new file mode 100644
index 0000000..7be4f04
--- /dev/null
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Intentionally blank - will contain shared Polymer 2 test setup code.
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index bc705a8..9448662 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -19,8 +19,8 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>Elements Test Runner</title>
 <meta charset="utf-8">
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
 <script>
   const testFiles = [];
   const elementsPath = '../elements/';
diff --git a/resources/com/google/gerrit/server/mail/DeleteKey.soy b/resources/com/google/gerrit/server/mail/DeleteKey.soy
new file mode 100644
index 0000000..30548c8
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/DeleteKey.soy
@@ -0,0 +1,72 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .DeleteKey template will determine the contents of the email related to
+ * deleting a SSH or GPG key.
+ */
+{template .DeleteKey kind="text"}
+  {@param email: ?}
+  One or more {$email.keyType} keys have been deleted on Gerrit Code Review at
+  {sp}{$email.gerritHost}:
+
+  {\n}
+  {\n}
+
+  {if $email.sshKey}
+    {$email.sshKey}
+  {elseif $email.gpgKeyFingerprints}
+    {$email.gpgKeyFingerprints}
+  {/if}
+
+  {\n}
+  {\n}
+
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can also manage your {$email.keyType} keys by visiting
+  {\n}
+  {if $email.sshKey}
+    {$email.gerritUrl}#/settings/ssh-keys
+  {elseif $email.gpgKey}
+    {$email.gerritUrl}#/settings/gpg-keys
+  {/if}
+  {\n}
+  {if $email.userNameEmail}
+    (while signed in as {$email.userNameEmail})
+  {else}
+    (while signed in as {$email.email})
+  {/if}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy b/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy
new file mode 100644
index 0000000..1ab3955
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/DeleteKeyHtml.soy
@@ -0,0 +1,64 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .DeleteKeyHtml}
+  {@param email: ?}
+  <p>
+    One or more {$email.keyType} keys have been deleted on Gerrit Code Review
+    at {$email.gerritHost}:
+  </p>
+
+  {let $keyStyle kind="css"}
+    background: #f0f0f0;
+    border: 1px solid #ccc;
+    color: #555;
+    padding: 12px;
+    width: 400px;
+  {/let}
+
+  {if $email.sshKey}
+    <pre style="{$keyStyle}">{$email.sshKey}</pre>
+  {elseif $email.gpgKeyFingerprints}
+    <pre style="{$keyStyle}">{$email.gpgKeyFingerprints}</pre>
+  {/if}
+
+  <p>
+    If this is not expected, please contact your Gerrit Administrators
+    immediately.
+  </p>
+
+  <p>
+    You can also manage your {$email.keyType} keys by following{sp}
+    {if $email.sshKey}
+      <a href="{$email.gerritUrl}#/settings/ssh-keys">this link</a>
+    {elseif $email.gpgKeyFingerprints}
+      <a href="{$email.gerritUrl}#/settings/gpg-keys">this link</a>
+    {/if}
+    {sp}
+    {if $email.userNameEmail}
+      (while signed in as {$email.userNameEmail})
+    {else}
+      (while signed in as {$email.email})
+    {/if}.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy b/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
new file mode 100644
index 0000000..38e679e
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/HttpPasswordUpdate.soy
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .HttpPasswordUpdate template will determine the contents of the email related to
+ * adding, changing or deleting the HTTP password.
+ */
+{template .HttpPasswordUpdate kind="text"}
+  {@param email: ?}
+  The HTTP password was {$email.operation} on Gerrit Code Review at
+  {sp}{$email.gerritHost}.
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can also manage your HTTP password by visiting
+  {\n}
+  {$email.gerritUrl}#/settings/http-password
+  {\n}
+  {if $email.userNameEmail}
+    (while signed in as {$email.userNameEmail})
+  {else}
+    (while signed in as {$email.email})
+  {/if}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy b/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
new file mode 100644
index 0000000..3c4594c
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/HttpPasswordUpdateHtml.soy
@@ -0,0 +1,46 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .HttpPasswordUpdateHtml}
+  {@param email: ?}
+  <p>
+    The HTTP password was {$email.operation} on Gerrit Code Review
+    at {$email.gerritHost}.
+  </p>
+
+  <p>
+    If this is not expected, please contact your Gerrit Administrators
+    immediately.
+  </p>
+
+  <p>
+    You can also manage your HTTP password by following{sp}
+    <a href="{$email.gerritUrl}#/settings/http-password">this link</a>
+    {sp}
+    {if $email.userNameEmail}
+      (while signed in as {$email.userNameEmail})
+    {else}
+      (while signed in as {$email.email})
+    {/if}.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/tools/BUILD b/tools/BUILD
index 6266456..09adf16 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -23,70 +23,74 @@
     visibility = ["//visibility:public"],
 )
 
-# This EP warnings list is based on:
+# Error Prone errors enabled by default; see ../.bazelrc for how this is
+# enabled. This warnings list is originally based on:
 # https://github.com/bazelbuild/BUILD_file_generator/blob/master/tools/bazel_defs/java.bzl
+# However, feel free to add any additional errors. Thus far they have all been pretty useful.
 java_package_configuration(
     name = "error_prone",
     javacopts = [
         "-XepDisableWarningsInGeneratedCode",
-        "-Xep:MissingCasesInEnumSwitch:ERROR",
-        "-Xep:ReferenceEquality:WARN",
-        "-Xep:StringEquality:WARN",
-        "-Xep:WildcardImport:WARN",
-        "-Xep:AmbiguousMethodReference:WARN",
-        "-Xep:BadAnnotationImplementation:WARN",
-        "-Xep:BadComparable:WARN",
+        "-Xep:AmbiguousMethodReference:ERROR",
+        "-Xep:BadAnnotationImplementation:ERROR",
+        "-Xep:BadComparable:ERROR",
         "-Xep:BoxedPrimitiveConstructor:ERROR",
-        "-Xep:CannotMockFinalClass:WARN",
-        "-Xep:ClassCanBeStatic:WARN",
-        "-Xep:ClassNewInstance:WARN",
+        "-Xep:CannotMockFinalClass:ERROR",
+        "-Xep:ClassCanBeStatic:ERROR",
+        "-Xep:ClassNewInstance:ERROR",
         "-Xep:DefaultCharset:ERROR",
-        "-Xep:DoubleCheckedLocking:WARN",
-        "-Xep:ElementsCountedInLoop:WARN",
-        "-Xep:EqualsHashCode:WARN",
-        "-Xep:EqualsIncompatibleType:WARN",
+        "-Xep:DoubleCheckedLocking:ERROR",
+        "-Xep:ElementsCountedInLoop:ERROR",
+        "-Xep:DoubleCheckedLocking:ERROR",
+        "-Xep:ElementsCountedInLoop:ERROR",
+        "-Xep:EqualsHashCode:ERROR",
+        "-Xep:EqualsIncompatibleType:ERROR",
         "-Xep:ExpectedExceptionChecker:ERROR",
-        "-Xep:Finally:WARN",
-        "-Xep:FloatingPointLiteralPrecision:WARN",
-        "-Xep:FragmentInjection:WARN",
-        "-Xep:FragmentNotInstantiable:WARN",
-        "-Xep:FunctionalInterfaceClash:WARN",
-        "-Xep:FutureReturnValueIgnored:WARN",
-        "-Xep:GetClassOnEnum:WARN",
-        "-Xep:ImmutableAnnotationChecker:WARN",
-        "-Xep:ImmutableEnumChecker:WARN",
-        "-Xep:IncompatibleModifiers:WARN",
-        "-Xep:InjectOnConstructorOfAbstractClass:WARN",
-        "-Xep:InputStreamSlowMultibyteRead:WARN",
-        "-Xep:IterableAndIterator:WARN",
-        "-Xep:JUnit3FloatingPointComparisonWithoutDelta:WARN",
-        "-Xep:JUnitAmbiguousTestClass:WARN",
-        "-Xep:LiteralClassName:WARN",
-        "-Xep:MissingFail:WARN",
-        "-Xep:MissingOverride:WARN",
+        "-Xep:Finally:ERROR",
+        "-Xep:FloatingPointLiteralPrecision:ERROR",
+        "-Xep:FragmentInjection:ERROR",
+        "-Xep:FragmentNotInstantiable:ERROR",
+        "-Xep:FunctionalInterfaceClash:ERROR",
+        "-Xep:FutureReturnValueIgnored:ERROR",
+        "-Xep:GetClassOnEnum:ERROR",
+        "-Xep:ImmutableAnnotationChecker:ERROR",
+        "-Xep:ImmutableEnumChecker:ERROR",
+        "-Xep:IncompatibleModifiers:ERROR",
+        "-Xep:InjectOnConstructorOfAbstractClass:ERROR",
+        "-Xep:InputStreamSlowMultibyteRead:ERROR",
+        "-Xep:IterableAndIterator:ERROR",
+        "-Xep:JUnit3FloatingPointComparisonWithoutDelta:ERROR",
+        "-Xep:JUnitAmbiguousTestClass:ERROR",
+        "-Xep:LiteralClassName:ERROR",
+        "-Xep:MissingCasesInEnumSwitch:ERROR",
+        "-Xep:MissingFail:ERROR",
+        "-Xep:MissingOverride:ERROR",
         "-Xep:MutableConstantField:ERROR",
-        "-Xep:NarrowingCompoundAssignment:WARN",
-        "-Xep:NonAtomicVolatileUpdate:WARN",
-        "-Xep:NonOverridingEquals:WARN",
-        "-Xep:NullableConstructor:WARN",
-        "-Xep:NullablePrimitive:WARN",
-        "-Xep:NullableVoid:WARN",
-        "-Xep:OperatorPrecedence:WARN",
-        "-Xep:OverridesGuiceInjectableMethod:WARN",
-        "-Xep:PreconditionsInvalidPlaceholder:WARN",
-        "-Xep:ProtoFieldPreconditionsCheckNotNull:WARN",
-        "-Xep:ProtocolBufferOrdinal:WARN",
-        "-Xep:RequiredModifiers:WARN",
-        "-Xep:ShortCircuitBoolean:WARN",
-        "-Xep:SimpleDateFormatConstant:WARN",
-        "-Xep:StaticGuardedByInstance:WARN",
-        "-Xep:SynchronizeOnNonFinalField:WARN",
-        "-Xep:TruthConstantAsserts:WARN",
-        "-Xep:TypeParameterShadowing:WARN",
-        "-Xep:TypeParameterUnusedInFormals:WARN",
-        "-Xep:URLEqualsHashCode:WARN",
-        "-Xep:UnsynchronizedOverridesSynchronized:WARN",
-        "-Xep:WaitNotInLoop:WARN",
+        "-Xep:NarrowingCompoundAssignment:ERROR",
+        "-Xep:NonAtomicVolatileUpdate:ERROR",
+        "-Xep:NonOverridingEquals:ERROR",
+        "-Xep:NullableConstructor:ERROR",
+        "-Xep:NullablePrimitive:ERROR",
+        "-Xep:NullableVoid:ERROR",
+        "-Xep:OperatorPrecedence:ERROR",
+        "-Xep:OverridesGuiceInjectableMethod:ERROR",
+        "-Xep:PreconditionsInvalidPlaceholder:ERROR",
+        "-Xep:ProtoFieldPreconditionsCheckNotNull:ERROR",
+        "-Xep:ProtocolBufferOrdinal:ERROR",
+        "-Xep:ReferenceEquality:ERROR",
+        "-Xep:RequiredModifiers:ERROR",
+        "-Xep:ShortCircuitBoolean:ERROR",
+        "-Xep:SimpleDateFormatConstant:ERROR",
+        "-Xep:StaticGuardedByInstance:ERROR",
+        "-Xep:StringEquality:ERROR",
+        "-Xep:SynchronizeOnNonFinalField:ERROR",
+        "-Xep:TruthConstantAsserts:ERROR",
+        "-Xep:TypeParameterShadowing:ERROR",
+        "-Xep:TypeParameterUnusedInFormals:ERROR",
+        "-Xep:URLEqualsHashCode:ERROR",
+        "-Xep:UnsynchronizedOverridesSynchronized:ERROR",
+        "-Xep:WaitNotInLoop:ERROR",
+        "-Xep:WildcardImport:ERROR",
     ],
     packages = ["error_prone_packages"],
 )
@@ -96,5 +100,16 @@
     packages = [
         "//java/...",
         "//javatests/...",
+        "//plugins/codemirror-editor/...",
+        "//plugins/commit-message-length-validator/...",
+        "//plugins/delete-project/...",
+        "//plugins/download-commands/...",
+        "//plugins/gitiles/...",
+        "//plugins/hooks/...",
+        "//plugins/plugin-manager/...",
+        "//plugins/replication/...",
+        "//plugins/reviewnotes/...",
+        "//plugins/singleusergroup/...",
+        "//plugins/webhooks/...",
     ],
 )
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 83c13a3..a4080e5 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -304,7 +304,14 @@
     else:
         bundled = ctx.outputs.html
     destdir = ctx.outputs.html.path + ".dir"
-    zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles]
+    zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles.to_list()]
+
+    # We are splitting off the package dir from the app.path such that
+    # we can set the package dir as the root for the bundler, which means
+    # that absolute imports are interpreted relative to that root.
+    pkg_dir = ctx.attr.pkg.lstrip("/")
+    app_path = ctx.file.app.path
+    app_path = app_path[app_path.index(pkg_dir) + len(pkg_dir):]
 
     hermetic_npm_binary = " ".join([
         "python",
@@ -315,10 +322,11 @@
         "--strip-comments",
         "--out-file",
         "$p/" + bundled.path,
-        ctx.file.app.path,
+        "--root",
+        pkg_dir,
+        app_path,
     ])
 
-    pkg_dir = ctx.attr.pkg.lstrip("/")
     cmd = " && ".join([
         # unpack dependencies.
         "export PATH",
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 5da5f05..7e5de3e 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -35,7 +35,7 @@
     return -1
 
 def _AsClassName(fname):
-    fname = [x.path for x in fname.files][0]
+    fname = [x.path for x in fname.files.to_list()][0]
     toks = fname[:-5].split("/")
     findex = -1
     for s in _PREFIXES: