Merge "Fix submodule plugins/replication to point to current head commit"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 07c80f5..2fd515f 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,11 +38,13 @@
 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;
@@ -52,7 +54,6 @@
 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.common.data.PermissionRule.Action;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -277,6 +278,9 @@
   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;
@@ -286,7 +290,6 @@
   @Inject private PluginGuiceEnvironment pluginGuiceEnvironment;
   @Inject private PluginUser.Factory pluginUserFactory;
   @Inject private ProjectIndexCollection projectIndexes;
-  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private SitePaths sitePaths;
 
@@ -884,23 +887,23 @@
     allow(project, ref, permission, id);
   }
 
-  protected void allow(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.allow(u.getConfig(), permission, id, ref);
-      u.save();
-    }
+  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 {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.allow(
-            u.getConfig(), capabilityName, id, new PermissionRange(capabilityName, min, max));
-      }
-      u.save();
-    }
+    // 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)
@@ -910,12 +913,13 @@
 
   protected void allowGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
       throws Exception {
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      for (String capabilityName : capabilityNames) {
-        Util.allow(u.getConfig(), capabilityName, id);
-      }
-      u.save();
-    }
+    // 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)
@@ -957,44 +961,52 @@
 
   protected void deny(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
       throws Exception {
-    try (ProjectConfigUpdate u = updateProject(p)) {
-      Util.deny(u.getConfig(), permission, id, ref);
-      u.save();
-    }
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(TestProjectUpdate.deny(permission).ref(ref).group(id))
+        .update();
   }
 
-  protected PermissionRule block(String ref, String permission, AccountGroup.UUID id)
-      throws Exception {
-    return block(project, ref, permission, id);
+  protected void block(String ref, String permission, AccountGroup.UUID id) throws Exception {
+    block(project, ref, permission, id);
   }
 
-  protected PermissionRule block(
-      Project.NameKey project, String ref, String permission, AccountGroup.UUID id)
+  protected void block(Project.NameKey project, String ref, String permission, AccountGroup.UUID id)
       throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      PermissionRule rule = Util.block(u.getConfig(), permission, id, ref);
-      u.save();
-      return rule;
-    }
+    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 {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      Util.block(u.getConfig(), Permission.LABEL + label, min, max, id, ref);
-      u.save();
-    }
+    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)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    grant(project, ref, permission, false);
+    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)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    grant(project, ref, permission, force, adminGroupUuid());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(TestProjectUpdate.allow(permission).ref(ref).group(adminGroupUuid()).force(force))
+        .update();
   }
 
   protected void grant(
@@ -1004,17 +1016,11 @@
       boolean force,
       AccountGroup.UUID groupUUID)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      PermissionRule rule = Util.newRule(config, groupUUID);
-      rule.setForce(force);
-      p.add(rule);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(TestProjectUpdate.allow(permission).ref(ref).group(groupUUID).force(force))
+        .update();
   }
 
   protected void grantLabel(
@@ -1023,25 +1029,19 @@
       int max,
       Project.NameKey project,
       String ref,
-      boolean force,
       AccountGroup.UUID groupUUID,
       boolean exclusive)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    String permission = Permission.LABEL + label;
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      md.setMessage(String.format("Grant %s on %s", permission, ref));
-      ProjectConfig config = projectConfigFactory.read(md);
-      AccessSection s = config.getAccessSection(ref, true);
-      Permission p = s.getPermission(permission, true);
-      p.setExclusiveGroup(exclusive);
-      PermissionRule rule = Util.newRule(config, groupUUID);
-      rule.setForce(force);
-      rule.setMin(min);
-      rule.setMax(max);
-      p.add(rule);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
+    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)
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 0714d22..ada2fb6 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -35,6 +35,7 @@
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/sshd",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//lib:args4j",
         "//lib:gson",
         "//lib:guava-retrying",
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
index 029d161..fc4caf8 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectConfig;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 /**
@@ -40,5 +42,33 @@
      * fully qualified refname ("refs/heads/master").
      */
     boolean hasHead(String branchName);
+
+    /** Returns a fresh {@link ProjectConfig} read from the tip of {@code refs/meta/config}. */
+    ProjectConfig getProjectConfig();
+
+    /**
+     * Returns a fresh JGit {@link Config} instance read from {@code project.config} at the tip of
+     * {@code refs/meta/config}. Does not have a base config, i.e. does not respect {@code
+     * $site_path/etc/project.config}.
+     */
+    Config getConfig();
+
+    /**
+     * Starts the fluent chain to update a project. The returned builder can be used to specify how
+     * the attributes of the project should be modified. To update the project for real, the {@link
+     * TestProjectUpdate.Builder#update()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * projectOperations
+     *     .forUpdate()
+     *     .add(allow(ABANDON).ref("refs/*").group(REGISTERED_USERS))
+     *     .update();
+     * </pre>
+     *
+     * @return a builder to update the check.
+     */
+    TestProjectUpdate.Builder forUpdate();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 4180aaa..6835ae4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -14,31 +14,62 @@
 
 package com.google.gerrit.acceptance.testsuite.project;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 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.Permission;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 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;
 import java.util.Collections;
 import org.apache.commons.lang.RandomStringUtils;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 
 public class ProjectOperationsImpl implements ProjectOperations {
-  private final ProjectCreator projectCreator;
   private final GitRepositoryManager repoManager;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final ProjectCache projectCache;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final ProjectCreator projectCreator;
 
   @Inject
-  ProjectOperationsImpl(GitRepositoryManager repoManager, ProjectCreator projectCreator) {
+  ProjectOperationsImpl(
+      GitRepositoryManager repoManager,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+      ProjectCache projectCache,
+      ProjectConfig.Factory projectConfigFactory,
+      ProjectCreator projectCreator) {
     this.repoManager = repoManager;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectCache = projectCache;
+    this.projectConfigFactory = projectConfigFactory;
     this.projectCreator = projectCreator;
   }
 
@@ -68,7 +99,6 @@
   }
 
   private class PerProjectOperations implements ProjectOperations.PerProjectOperations {
-
     Project.NameKey nameKey;
 
     PerProjectOperations(Project.NameKey nameKey) {
@@ -85,6 +115,60 @@
       return headOrNull(branch) != null;
     }
 
+    @Override
+    public TestProjectUpdate.Builder forUpdate() {
+      return TestProjectUpdate.builder(this::updateProject);
+    }
+
+    private void updateProject(TestProjectUpdate projectUpdate)
+        throws IOException, ConfigInvalidException {
+      try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) {
+        ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate);
+        addCapabilities(projectConfig, projectUpdate.addedCapabilities());
+        addPermissions(projectConfig, projectUpdate.addedPermissions());
+        addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
+        projectConfig.commit(metaDataUpdate);
+      }
+      projectCache.evict(nameKey);
+    }
+
+    private void addCapabilities(
+        ProjectConfig projectConfig, ImmutableList<TestCapability> addedCapabilities) {
+      for (TestCapability c : addedCapabilities) {
+        PermissionRule rule = Util.newRule(projectConfig, c.group());
+        rule.setRange(c.min(), c.max());
+        projectConfig
+            .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+            .getPermission(c.name(), true)
+            .add(rule);
+      }
+    }
+
+    private void addPermissions(
+        ProjectConfig projectConfig, ImmutableList<TestPermission> addedPermissions) {
+      for (TestPermission p : addedPermissions) {
+        PermissionRule rule = Util.newRule(projectConfig, p.group());
+        rule.setAction(p.action());
+        rule.setForce(p.force());
+        projectConfig.getAccessSection(p.ref(), true).getPermission(p.name(), true).add(rule);
+      }
+    }
+
+    private void addLabelPermissions(
+        ProjectConfig projectConfig, ImmutableList<TestLabelPermission> addedLabelPermissions) {
+      for (TestLabelPermission p : addedLabelPermissions) {
+        PermissionRule rule = Util.newRule(projectConfig, p.group());
+        rule.setAction(p.action());
+        rule.setRange(p.min(), p.max());
+        Permission permission =
+            projectConfig
+                .getAccessSection(p.ref(), true)
+                .getPermission(Permission.forLabel(p.name()), true);
+        permission.setExclusiveGroup(p.exclusive());
+        permission.add(rule);
+      }
+    }
+
     private RevCommit headOrNull(String branch) {
       if (!branch.startsWith(Constants.R_REFS)) {
         branch = RefNames.REFS_HEADS + branch;
@@ -98,5 +182,39 @@
         throw new IllegalStateException(e);
       }
     }
+
+    @Override
+    public ProjectConfig getProjectConfig() {
+      try (Repository repo = repoManager.openRepository(nameKey)) {
+        ProjectConfig projectConfig = projectConfigFactory.create(nameKey);
+        projectConfig.load(nameKey, repo);
+        return projectConfig;
+      } catch (Exception e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    @Override
+    public Config getConfig() {
+      try (Repository repo = repoManager.openRepository(nameKey);
+          RevWalk rw = new RevWalk(repo)) {
+        Ref ref = repo.exactRef(REFS_CONFIG);
+        if (ref == null) {
+          return new Config();
+        }
+        RevTree tree = rw.parseTree(ref.getObjectId());
+        TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), PROJECT_CONFIG, tree);
+        if (tw == null) {
+          return new Config();
+        }
+        ObjectLoader loader = rw.getObjectReader().open(tw.getObjectId(0));
+        String text = new String(loader.getCachedBytes(), UTF_8);
+        Config config = new Config();
+        config.fromText(text);
+        return config;
+      } catch (Exception e) {
+        throw new IllegalStateException(e);
+      }
+    }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
new file mode 100644
index 0000000..b58eae6
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -0,0 +1,288 @@
+// 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.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.LabelType;
+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 java.util.Optional;
+
+@AutoValue
+public abstract class TestProjectUpdate {
+  /** Starts a builder for allowing a capability. */
+  public static TestCapability.Builder allowCapability(String name) {
+    return TestCapability.builder().name(name);
+  }
+
+  /** Records a global capability to be updated. */
+  @AutoValue
+  public abstract static class TestCapability {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestCapability.Builder();
+    }
+
+    abstract String name();
+
+    abstract AccountGroup.UUID group();
+
+    abstract int min();
+
+    abstract int max();
+
+    /** Builder for {@link TestCapability}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      /** Sets the name of the capability. */
+      public abstract Builder name(String name);
+
+      abstract String name();
+
+      /** Sets the group to which the capability applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      abstract Builder min(int min);
+
+      abstract Optional<Integer> min();
+
+      abstract Builder max(int max);
+
+      abstract Optional<Integer> max();
+
+      /** Sets the minimum and maximum values for the capability. */
+      public Builder range(int min, int max) {
+        return min(min).max(max);
+      }
+
+      /** Builds the {@link TestCapability}. */
+      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 (!max().isPresent()) {
+          max(withDefaults != null ? withDefaults.getDefaultMax() : 0);
+        }
+        return autoBuild();
+      }
+    }
+  }
+
+  /** Starts a builder for allowing a permission. */
+  public static TestPermission.Builder allow(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+  }
+
+  /** Starts a builder for denying a permission. */
+  public static TestPermission.Builder deny(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.DENY);
+  }
+
+  /** Starts a builder for blocking a permission. */
+  public static TestPermission.Builder block(String name) {
+    return TestPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+  }
+
+  /**
+   * Records a permission to be updated.
+   *
+   * <p>Not used for permissions that have ranges (label permissions) or global capabilities.
+   */
+  @AutoValue
+  public abstract static class TestPermission {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestPermission.Builder().force(false);
+    }
+
+    abstract String name();
+
+    abstract String ref();
+
+    abstract AccountGroup.UUID group();
+
+    abstract PermissionRule.Action action();
+
+    abstract boolean force();
+
+    /** Builder for {@link TestPermission}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder name(String name);
+
+      /** Sets the ref pattern used on the permission. */
+      public abstract Builder ref(String ref);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID groupUuid);
+
+      abstract Builder action(PermissionRule.Action action);
+
+      /** Sets whether the permission is a force permission. */
+      public abstract Builder force(boolean force);
+
+      /** Builds the {@link TestPermission}. */
+      public abstract TestPermission build();
+    }
+  }
+
+  /** Starts a builder for allowing a label permission. */
+  public static TestLabelPermission.Builder allowLabel(String name) {
+    return TestLabelPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+  }
+
+  /** Starts a builder for denying a label permission. */
+  public static TestLabelPermission.Builder blockLabel(String name) {
+    return TestLabelPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+  }
+
+  /** Records a label permission to be updated. */
+  @AutoValue
+  public abstract static class TestLabelPermission {
+    private static Builder builder() {
+      return new AutoValue_TestProjectUpdate_TestLabelPermission.Builder().exclusive(false);
+    }
+
+    abstract String name();
+
+    abstract String ref();
+
+    abstract AccountGroup.UUID group();
+
+    abstract PermissionRule.Action action();
+
+    abstract int min();
+
+    abstract int max();
+
+    abstract boolean exclusive();
+
+    /** Builder for {@link TestLabelPermission}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      abstract Builder name(String name);
+
+      /** Sets the ref pattern used on the permission. */
+      public abstract Builder ref(String ref);
+
+      /** Sets the group to which the permission applies. */
+      public abstract Builder group(AccountGroup.UUID group);
+
+      abstract Builder action(PermissionRule.Action action);
+
+      abstract Builder min(int min);
+
+      abstract Builder max(int max);
+
+      /** Sets the minimum and maximum values for the permission. */
+      public Builder range(int min, int 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);
+
+      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());
+        return result;
+      }
+    }
+  }
+
+  static Builder builder(ThrowingConsumer<TestProjectUpdate> projectUpdater) {
+    return new AutoValue_TestProjectUpdate.Builder().projectUpdater(projectUpdater);
+  }
+
+  /** Builder for {@link TestProjectUpdate}. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    abstract ImmutableList.Builder<TestPermission> addedPermissionsBuilder();
+
+    abstract ImmutableList.Builder<TestLabelPermission> addedLabelPermissionsBuilder();
+
+    abstract ImmutableList.Builder<TestCapability> addedCapabilitiesBuilder();
+
+    /** Adds a permission to be included in this update. */
+    public Builder add(TestPermission testPermission) {
+      addedPermissionsBuilder().add(testPermission);
+      return this;
+    }
+
+    /** Adds a permission to be included in this update. */
+    public Builder add(TestPermission.Builder testPermissionBuilder) {
+      return add(testPermissionBuilder.build());
+    }
+
+    /** Adds a label permission to be included in this update. */
+    public Builder add(TestLabelPermission testLabelPermission) {
+      addedLabelPermissionsBuilder().add(testLabelPermission);
+      return this;
+    }
+
+    /** Adds a label permission to be included in this update. */
+    public Builder add(TestLabelPermission.Builder testLabelPermissionBuilder) {
+      return add(testLabelPermissionBuilder.build());
+    }
+
+    /** Adds a capability to be included in this update. */
+    public Builder add(TestCapability testCapability) {
+      addedCapabilitiesBuilder().add(testCapability);
+      return this;
+    }
+
+    /** Adds a capability to be included in this update. */
+    public Builder add(TestCapability.Builder testCapabilityBuilder) {
+      return add(testCapabilityBuilder.build());
+    }
+
+    abstract Builder projectUpdater(ThrowingConsumer<TestProjectUpdate> projectUpdater);
+
+    abstract TestProjectUpdate autoBuild();
+
+    /** Executes the update, updating the underlying project. */
+    public void update() {
+      TestProjectUpdate projectUpdate = autoBuild();
+      projectUpdate.projectUpdater().acceptAndThrowSilently(projectUpdate);
+    }
+  }
+
+  abstract ImmutableList<TestPermission> addedPermissions();
+
+  abstract ImmutableList<TestLabelPermission> addedLabelPermissions();
+
+  abstract ImmutableList<TestCapability> addedCapabilities();
+
+  abstract ThrowingConsumer<TestProjectUpdate> projectUpdater();
+}
diff --git a/java/com/google/gerrit/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index 6f958b1..4727da1 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -6,6 +6,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/truth/ConfigSubject.java b/java/com/google/gerrit/truth/ConfigSubject.java
new file mode 100644
index 0000000..2a99151
--- /dev/null
+++ b/java/com/google/gerrit/truth/ConfigSubject.java
@@ -0,0 +1,129 @@
+// 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.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.truth.BooleanSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.ListMultimapSubject;
+import com.google.common.truth.LongSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.common.Nullable;
+import java.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+
+public class ConfigSubject extends Subject<ConfigSubject, Config> {
+  public static ConfigSubject assertThat(Config config) {
+    return assertAbout(ConfigSubject::new).that(config);
+  }
+
+  private final Config config;
+
+  private ConfigSubject(FailureMetadata metadata, Config actual) {
+    super(metadata, actual);
+    this.config = actual;
+  }
+
+  public IterableSubject sections() {
+    isNotNull();
+    return check("getSections()").that(config.getSections());
+  }
+
+  public IterableSubject subsections(String section) {
+    requireNonNull(section);
+    isNotNull();
+    return check("getSubsections(%s)", section).that(config.getSubsections(section));
+  }
+
+  public ListMultimapSubject sectionValues(String section) {
+    requireNonNull(section);
+    return sectionValuesImpl(section, null);
+  }
+
+  public ListMultimapSubject subsectionValues(String section, String subsection) {
+    requireNonNull(section);
+    requireNonNull(subsection);
+    return sectionValuesImpl(section, subsection);
+  }
+
+  private ListMultimapSubject sectionValuesImpl(String section, @Nullable String subsection) {
+    isNotNull();
+    ImmutableListMultimap.Builder<String, String> b = ImmutableListMultimap.builder();
+    config
+        .getNames(section, subsection, true)
+        .forEach(
+            n ->
+                Arrays.stream(config.getStringList(section, subsection, n))
+                    .forEach(v -> b.put(n, v)));
+    return check("getSection(%s, %s)", section, subsection).that(b.build());
+  }
+
+  public void isEmpty() {
+    sections().isEmpty();
+  }
+
+  public StringSubject text() {
+    isNotNull();
+    return check("toText()").that(config.toText());
+  }
+
+  public IterableSubject stringValues(String section, @Nullable String subsection, String name) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getStringList(%s, %s, %s)", section, subsection, name)
+        .that(Arrays.asList(config.getStringList(section, subsection, name)));
+  }
+
+  public StringSubject stringValue(String section, @Nullable String subsection, String name) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getString(%s, %s, %s)", section, subsection, name)
+        .that(config.getString(section, subsection, name));
+  }
+
+  public IntegerSubject intValue(
+      String section, @Nullable String subsection, String name, int defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getInt(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getInt(section, subsection, name, defaultValue));
+  }
+
+  public LongSubject longValue(String section, String subsection, String name, long defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getLong(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getLong(section, subsection, name, defaultValue));
+  }
+
+  public BooleanSubject booleanValue(
+      String section, String subsection, String name, boolean defaultValue) {
+    requireNonNull(section);
+    requireNonNull(name);
+    isNotNull();
+    return check("getBoolean(%s, %s, %s, %s)", section, subsection, name, defaultValue)
+        .that(config.getBoolean(section, subsection, name, defaultValue));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/BUILD b/javatests/com/google/gerrit/acceptance/BUILD
index 54b3626..405610b 100644
--- a/javatests/com/google/gerrit/acceptance/BUILD
+++ b/javatests/com/google/gerrit/acceptance/BUILD
@@ -7,6 +7,7 @@
         "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
index 49c23e3..3d17de0 100644
--- a/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
+++ b/javatests/com/google/gerrit/acceptance/MergeableFileBasedConfigTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
@@ -111,7 +112,7 @@
   }
 
   private void assertConfig(MergeableFileBasedConfig cfg, String expected) throws Exception {
-    assertThat(cfg.toText()).isEqualTo(expected);
+    assertThat(cfg).text().isEqualTo(expected);
     cfg.save();
     assertThat(new String(Files.readAllBytes(cfg.getFile().toPath()), UTF_8)).isEqualTo(expected);
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 6e35c2d..fc04204 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -33,6 +33,7 @@
 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;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -477,10 +478,11 @@
           assertThat(tw).isNotNull();
           Config cfg = new Config();
           cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8));
-          assertThat(
-                  cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_FULL_NAME))
+          assertThat(cfg)
+              .stringValue(AccountProperties.ACCOUNT, null, AccountProperties.KEY_FULL_NAME)
               .isEqualTo(name);
-          assertThat(cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS))
+          assertThat(cfg)
+              .stringValue(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS)
               .isEqualTo(status);
         } else {
           // No account properties were set, hence an 'account.config' file was not created.
@@ -1537,7 +1539,7 @@
     accountIndexedCounter.clear();
 
     grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
-    grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroupUuid(), false);
+    grantLabel("Code-Review", -2, 2, allUsers, userRef, adminGroupUuid(), false);
     grant(allUsers, userRef, Permission.SUBMIT, false, adminGroupUuid());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 09c0631..28ebadb 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -1415,8 +1415,7 @@
 
   private void pushToGroupBranchForReviewAndSubmit(
       Project.NameKey project, String groupRef, String expectedError) throws Throwable {
-    grantLabel(
-        "Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", false, REGISTERED_USERS, false);
+    grantLabel("Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", REGISTERED_USERS, false);
     grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> repo = cloneProject(project);
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index eb4845e..b895ddf 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -82,7 +82,7 @@
 
   @Test
   public void deleteNotAllowedWithOnlyPushPermission() throws Exception {
-    grant(project, "refs/*", Permission.PUSH, false);
+    grant(project, "refs/*", Permission.PUSH);
     assertDeleteRef(REJECTED_OTHER_REASON);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 75c17bf..39a29cc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -40,7 +40,6 @@
 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;
@@ -123,7 +122,6 @@
   @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;
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 41f3147..70abe24 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -140,7 +140,7 @@
 
   private void grantApprove(Project.NameKey project, AccountGroup.UUID groupUUID, boolean exclusive)
       throws Exception {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, groupUUID, exclusive);
+    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", groupUUID, exclusive);
   }
 
   private void blockApproveForChangeOwner(Project.NameKey project) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index 9a907aa..a4ca7a3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
@@ -36,10 +36,6 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Before;
 import org.junit.Test;
@@ -75,8 +71,8 @@
   }
 
   private String testUpdateProjectConfig() throws Exception {
-    Config cfg = readProjectConfig();
-    assertThat(cfg.getString("project", null, "description")).isNull();
+    Config cfg = projectOperations.project(project).getConfig();
+    assertThat(cfg).stringValue("project", null, "description").isNull();
     String desc = "new project description";
     cfg.setString("project", null, "description", desc);
 
@@ -89,7 +85,12 @@
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().description).isEqualTo(desc);
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("project", null, "description")).isEqualTo(desc);
+    assertThat(
+            projectOperations
+                .project(project)
+                .getConfig()
+                .getString("project", null, "description"))
+        .isEqualTo(desc);
     String changeRev = gApi.changes().id(id).get().currentRevision;
     String branchRev =
         gApi.projects().name(project.get()).branch(RefNames.REFS_CONFIG).get().revision;
@@ -107,8 +108,8 @@
     gApi.projects().create(parent);
 
     requestScopeOperations.setApiUser(user.id());
-    Config cfg = readProjectConfig();
-    assertThat(cfg.getString("access", null, "inheritFrom")).isAnyOf(null, allProjects.get());
+    Config cfg = projectOperations.project(project).getConfig();
+    assertThat(cfg).stringValue("access", null, "inheritFrom").isAnyOf(null, allProjects.get());
     cfg.setString("access", null, "inheritFrom", parent.name);
 
     PushOneCommit.Result r = createConfigChange(cfg);
@@ -133,7 +134,8 @@
 
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(allProjects.get());
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+    assertThat(
+            projectOperations.project(project).getConfig().getString("access", null, "inheritFrom"))
         .isAnyOf(null, allProjects.get());
 
     requestScopeOperations.setApiUser(admin.id());
@@ -141,7 +143,9 @@
     assertThat(gApi.changes().id(id).info().status).isEqualTo(ChangeStatus.MERGED);
     assertThat(gApi.projects().name(project.get()).get().parent).isEqualTo(parent.name);
     fetchRefsMetaConfig();
-    assertThat(readProjectConfig().getString("access", null, "inheritFrom")).isEqualTo(parent.name);
+    assertThat(
+            projectOperations.project(project).getConfig().getString("access", null, "inheritFrom"))
+        .isEqualTo(parent.name);
   }
 
   @Test
@@ -179,17 +183,6 @@
     testRepo.reset(RefNames.REFS_CONFIG);
   }
 
-  private Config readProjectConfig() throws Exception {
-    RevWalk rw = testRepo.getRevWalk();
-    RevTree tree = rw.parseTree(testRepo.getRepository().resolve("HEAD"));
-    RevObject obj = rw.parseAny(testRepo.get(tree, "project.config"));
-    ObjectLoader loader = rw.getObjectReader().open(obj);
-    String text = new String(loader.getCachedBytes(), UTF_8);
-    Config cfg = new Config();
-    cfg.fromText(text);
-    return cfg;
-  }
-
   private PushOneCommit.Result createConfigChange(Config cfg) throws Exception {
     PushOneCommit.Result r =
         pushFactory
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index f53ff23..ab9eed4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -607,7 +607,7 @@
 
   @Test
   public void dependencyOnChangeForNonVisibleBranchPreventsMerge() throws Throwable {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, REGISTERED_USERS, false);
+    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", REGISTERED_USERS, false);
     grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     // Create a change
@@ -662,7 +662,7 @@
 
   @Test
   public void dependencyOnHiddenChangePreventsMerge() throws Throwable {
-    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", false, REGISTERED_USERS, false);
+    grantLabel("Code-Review", -2, 2, project, "refs/heads/*", REGISTERED_USERS, false);
     grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     // Create a change
@@ -716,9 +716,9 @@
     Project.NameKey p1 = projectOperations.newProject().create();
     Project.NameKey p2 = projectOperations.newProject().create();
 
-    grantLabel("Code-Review", -2, 2, p1, "refs/heads/*", false, REGISTERED_USERS, false);
+    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/*", false, REGISTERED_USERS, false);
+    grantLabel("Code-Review", -2, 2, p2, "refs/heads/*", REGISTERED_USERS, false);
     grant(p2, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
 
     TestRepository<?> repo1 = cloneProject(p1);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 8d62765..503ebcc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -18,6 +18,7 @@
 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;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
@@ -568,7 +569,7 @@
             .file(ProjectConfig.PROJECT_CONFIG)
             .asString();
     cfg.fromText(config);
-    assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
+    assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
 
     // Make permission change through API
     ProjectAccessInput accessInput = newProjectAccessInput();
@@ -587,7 +588,7 @@
             .file(ProjectConfig.PROJECT_CONFIG)
             .asString();
     cfg.fromText(config);
-    assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
+    assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 9492536..f95342a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -23,6 +23,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.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;
@@ -134,8 +135,18 @@
 
   @Test
   public void deleteUserBranch_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(
+            TestProjectUpdate.allow(Permission.CREATE)
+                .ref(RefNames.REFS_USERS + "*")
+                .group(REGISTERED_USERS))
+        .add(
+            TestProjectUpdate.allow(Permission.PUSH)
+                .ref(RefNames.REFS_USERS + "*")
+                .group(REGISTERED_USERS))
+        .update();
 
     ResourceConflictException thrown =
         assertThrows(
@@ -159,7 +170,15 @@
   }
 
   private void blockForcePush() throws Exception {
-    block("refs/heads/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.block(Permission.PUSH)
+                .ref("refs/heads/*")
+                .group(ANONYMOUS_USERS)
+                .force(true))
+        .update();
   }
 
   private void grantForcePush() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index bc0e2ea..892c375 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -22,6 +22,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.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.TagApi;
@@ -98,7 +99,15 @@
   }
 
   private void blockForcePush() throws Exception {
-    block("refs/tags/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.block(Permission.PUSH)
+                .ref("refs/tags/*")
+                .group(ANONYMOUS_USERS)
+                .force(true))
+        .update();
   }
 
   private void grantForcePush() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 3f537c0..8ecd21c 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -15,14 +15,31 @@
 package com.google.gerrit.acceptance.testsuite.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+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.truth.ConfigSubject.assertThat;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.junit.Test;
 
 public class ProjectOperationsImplTest extends AbstractDaemonTest {
@@ -48,9 +65,322 @@
   @Test
   public void emptyCommit() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
+
     List<BranchInfo> branches = gApi.projects().name(key.get()).branches().get();
     assertThat(branches).isNotEmpty();
     assertThat(branches.stream().map(x -> x.ref).collect(toList()))
         .isEqualTo(ImmutableList.of("HEAD", "refs/meta/config", "refs/heads/master"));
   }
+
+  @Test
+  public void getProjectConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
+        .isEmpty();
+
+    ConfigInput input = new ConfigInput();
+    input.description = "my fancy project";
+    gApi.projects().name(key.get()).config(input);
+
+    assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
+        .isEqualTo("my fancy project");
+  }
+
+  @Test
+  public void mutatingResultOfGetProjectConfigDoesNotMutateGlobalCachedValue() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    ProjectConfig projectConfig = projectOperations.project(key).getProjectConfig();
+    ProjectState cachedProjectState1 = projectCache.checkedGet(key);
+    ProjectConfig cachedProjectConfig1 = cachedProjectState1.getConfig();
+    assertThat(cachedProjectConfig1).isNotSameInstanceAs(projectConfig);
+    assertThat(cachedProjectConfig1.getProject().getDescription()).isEmpty();
+    assertThat(projectConfig.getProject().getDescription()).isEmpty();
+    projectConfig.getProject().setDescription("my fancy project");
+
+    ProjectConfig cachedProjectConfig2 = projectCache.checkedGet(key).getConfig();
+    assertThat(cachedProjectConfig2).isNotSameInstanceAs(projectConfig);
+    assertThat(cachedProjectConfig2.getProject().getDescription()).isEmpty();
+  }
+
+  @Test
+  public void getProjectConfigNoRefsMetaConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    deleteRefsMetaConfig(key);
+
+    ProjectConfig projectConfig = projectOperations.project(key).getProjectConfig();
+    assertThat(projectConfig.getName()).isEqualTo(key);
+    assertThat(projectConfig.getRevision()).isNull();
+  }
+
+  @Test
+  public void getConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).text().isEmpty();
+
+    ConfigInput input = new ConfigInput();
+    input.description = "my fancy project";
+    gApi.projects().name(key.get()).config(input);
+
+    config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).sections().containsExactly("project");
+    assertThat(config).subsections("project").isEmpty();
+    assertThat(config).sectionValues("project").containsExactly("description", "my fancy project");
+  }
+
+  @Test
+  public void getConfigNoRefsMetaConfig() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    deleteRefsMetaConfig(key);
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).isNotInstanceOf(StoredConfig.class);
+    assertThat(config).isEmpty();
+  }
+
+  @Test
+  public void addAllowPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(TestProjectUpdate.allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .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");
+  }
+
+  @Test
+  public void addDenyPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(TestProjectUpdate.deny(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .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", "deny group global:Registered-Users");
+  }
+
+  @Test
+  public void addBlockPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(TestProjectUpdate.block(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS))
+        .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", "block group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowForcePermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.allow(Permission.ABANDON)
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .force(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", "+force group global:Registered-Users");
+  }
+
+  @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))
+        .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:Project-Owners",
+            "create", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addDuplicatePermissions() throws Exception {
+    TestPermission permission =
+        TestProjectUpdate.allow(Permission.ABANDON).ref("refs/foo").group(REGISTERED_USERS).build();
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations.project(key).forUpdate().add(permission).add(permission).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",
+            "abandon", "group global:Registered-Users");
+
+    projectOperations.project(key).forUpdate().add(permission).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",
+            "abandon", "group global:Registered-Users",
+            "abandon", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.allowLabel("Code-Review")
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .range(-1, 2))
+        .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("label-Code-Review", "-1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addBlockLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.blockLabel("Code-Review")
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .range(-1, 2))
+        .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("label-Code-Review", "block -1..+2 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowExclusiveLabelPermission() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.allowLabel("Code-Review")
+                .ref("refs/foo")
+                .group(REGISTERED_USERS)
+                .range(-1, 2)
+                .exclusive(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(
+            "label-Code-Review", "-1..+2 group global:Registered-Users",
+            "exclusiveGroupPermissions", "label-Code-Review");
+  }
+
+  @Test
+  public void addAllowCapability() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .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)
+        .sectionValues("capability")
+        .containsExactly("administrateServer", "group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowCapabilityWithRange() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .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)
+        .sectionValues("capability")
+        .containsExactly("queryLimit", "+0..+5000 group global:Registered-Users");
+  }
+
+  @Test
+  public void addAllowCapabilityWithDefaultRange() throws Exception {
+    Project.NameKey key = projectOperations.newProject().create();
+    projectOperations
+        .project(key)
+        .forUpdate()
+        .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS))
+        .update();
+
+    Config config = projectOperations.project(key).getConfig();
+    assertThat(config).sections().containsExactly("capability");
+    assertThat(config).subsections("capability").isEmpty();
+    assertThat(config)
+        .sectionValues("capability")
+        .containsExactly(
+            "queryLimit", "+0..+" + DEFAULT_MAX_QUERY_LIMIT + " group global:Registered-Users");
+  }
+
+  private void deleteRefsMetaConfig(Project.NameKey key) throws Exception {
+    try (Repository repo = repoManager.openRepository(key)) {
+      new TestRepository<>(repo).delete(REFS_CONFIG);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
index 231b584..865bda6 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -90,17 +91,17 @@
     Config cfg = new Config();
     ConfigUtil.storeSection(cfg, SECT, SUB, in, d);
 
-    assertThat(cfg.getString(SECT, SUB, "CONSTANT")).isNull();
-    assertThat(cfg.getString(SECT, SUB, "missing")).isNull();
-    assertThat(cfg.getBoolean(SECT, SUB, "b", false)).isEqualTo(in.b);
-    assertThat(cfg.getBoolean(SECT, SUB, "bb", false)).isEqualTo(in.bb);
-    assertThat(cfg.getInt(SECT, SUB, "i", 0)).isEqualTo(0);
-    assertThat(cfg.getInt(SECT, SUB, "ii", 0)).isEqualTo(in.ii);
-    assertThat(cfg.getLong(SECT, SUB, "l", 0L)).isEqualTo(0L);
-    assertThat(cfg.getLong(SECT, SUB, "ll", 0L)).isEqualTo(in.ll);
-    assertThat(cfg.getString(SECT, SUB, "s")).isEqualTo(in.s);
-    assertThat(cfg.getString(SECT, SUB, "sd")).isNull();
-    assertThat(cfg.getString(SECT, SUB, "nd")).isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "CONSTANT").isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "missing").isNull();
+    assertThat(cfg).booleanValue(SECT, SUB, "b", false).isEqualTo(in.b);
+    assertThat(cfg).booleanValue(SECT, SUB, "bb", false).isEqualTo(in.bb);
+    assertThat(cfg).intValue(SECT, SUB, "i", 0).isEqualTo(0);
+    assertThat(cfg).intValue(SECT, SUB, "ii", 0).isEqualTo(in.ii);
+    assertThat(cfg).longValue(SECT, SUB, "l", 0L).isEqualTo(0L);
+    assertThat(cfg).longValue(SECT, SUB, "ll", 0L).isEqualTo(in.ll);
+    assertThat(cfg).stringValue(SECT, SUB, "s").isEqualTo(in.s);
+    assertThat(cfg).stringValue(SECT, SUB, "sd").isNull();
+    assertThat(cfg).stringValue(SECT, SUB, "nd").isNull();
 
     SectionInfo out = new SectionInfo();
     ConfigUtil.loadSection(cfg, SECT, SUB, out, d, null);
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
index 5f13489..4c384e0 100644
--- a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.getAllProjectsWithoutDefaultAcls;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.getDefaultAllProjectsWithAllDefaultSections;
 import static com.google.gerrit.server.schema.testing.AllProjectsCreatorTestUtil.readAllProjectsConfig;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.GroupReference;
@@ -126,7 +127,7 @@
     allProjectsCreator.create(allProjectsInput);
 
     Config config = readAllProjectsConfig(repoManager, allProjectsName);
-    assertThat(config.getString("project", null, "description")).isEqualTo(testDescription);
+    assertThat(config).stringValue("project", null, "description").isEqualTo(testDescription);
   }
 
   @Test
@@ -142,7 +143,7 @@
     allProjectsCreator.create(allProjectsInput);
 
     Config config = readAllProjectsConfig(repoManager, allProjectsName);
-    assertThat(config.getBoolean("submit", null, "rejectEmptyCommit", false)).isTrue();
+    assertThat(config).booleanValue("submit", null, "rejectEmptyCommit", false).isTrue();
   }
 
   @Test