Merge "Support reading default project.config for All-Projects from etc"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e13a9b9..6362597 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -4959,6 +4959,38 @@
 Assuming that the server is started on `Mon 07:00` then this yields the
 first run on Tuesday at 06:00 and a repetition interval of 1 day.
 
+[[All-Projects-project.config]]
+== File `etc/All-Projects/project.config`
+
+The optional file `'$site_path'/etc/All-Projects/project.config` provides
+defaults for configuration read from
+link:config-project-config.html[`project.config`] in the
+`All-Projects` repo. Unlike `gerrit.config`, this file contains project-type
+configuration rather than server-type configuration.
+
+Most administrators will not need this file, and should instead make commits to
+`All-Projects` to modify global config. However, a separate file can be useful
+when managing multiple Gerrit servers, since pushing changes to defaults using
+Puppet or a similar tool can be easier than scripting git updates to
+`All-Projects`.
+
+The contents of the file are loaded each time the `All-Projects` project is
+reloaded. Updating the file requires either evicting the project cache or
+restarting the server.
+
+Caveats:
+
+* The path from which the file is read corresponds to the name of the repo,
+  which is link:#gerrit.allProjects[configurable].
+* Although the file lives in a directory that shares a name with a repository,
+  this directory is not a Git repository.
+* Only the file `project.config` is read from this directory to provide
+  defaults; any other files in this directory, such as `rules.pl`, are ignored.
+  (This behavior may change in the future.)
+* Group names listed in the access config in this file are resolved to UUIDs
+  using the `groups` file in the repository, not in the config directory. As a
+  result, setting ACLs in this file is not recommended.
+
 [[secure.config]]
 == File `etc/secure.config`
 
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 9fd3f16..20e7ba2 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.pgm.init.api;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 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.config.SitePaths;
 import com.google.gerrit.server.project.GroupList;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -27,16 +29,21 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.StoredConfig;
 
 public class AllProjectsConfig extends VersionedMetaDataOnInit {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  @Nullable private final StoredConfig baseConfig;
   private Config cfg;
   private GroupList groupList;
 
   @Inject
   AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site, InitFlags flags) {
     super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
+    this.baseConfig =
+        ProjectConfig.Factory.getBaseConfig(
+            site, new AllProjectsName(allProjects.get()), new Project.NameKey(allProjects.get()));
   }
 
   public Config getConfig() {
@@ -55,8 +62,11 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
     groupList = readGroupList();
-    cfg = readConfig(ProjectConfig.PROJECT_CONFIG);
+    cfg = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
   }
 
   private GroupList readGroupList() throws IOException {
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 196267e..b33aa3c 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -462,7 +462,12 @@
   }
 
   protected Config readConfig(String fileName) throws IOException, ConfigInvalidException {
-    Config rc = new Config();
+    return readConfig(fileName, null);
+  }
+
+  protected Config readConfig(String fileName, Config baseConfig)
+      throws IOException, ConfigInvalidException {
+    Config rc = new Config(baseConfig);
     String text = readUTF8(fileName);
     if (!text.isEmpty()) {
       try {
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 74c0f3b..b16b076 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -52,13 +52,16 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -83,8 +86,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.util.FS;
 
 public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   public static final String COMMENTLINK = "commentlink";
@@ -173,8 +179,28 @@
   // ProjectCache, so this would retain lots more memory.
   @Singleton
   public static class Factory {
+    @Nullable
+    public static StoredConfig getBaseConfig(
+        SitePaths sitePaths, AllProjectsName allProjects, Project.NameKey projectName) {
+      return projectName.equals(allProjects)
+          // Delay loading till onLoad method.
+          ? new FileBasedConfig(
+              sitePaths.etc_dir.resolve(allProjects.get()).resolve(PROJECT_CONFIG).toFile(),
+              FS.DETECTED)
+          : null;
+    }
+
+    private final SitePaths sitePaths;
+    private final AllProjectsName allProjects;
+
+    @Inject
+    Factory(SitePaths sitePaths, AllProjectsName allProjects) {
+      this.sitePaths = sitePaths;
+      this.allProjects = allProjects;
+    }
+
     public ProjectConfig create(Project.NameKey projectName) {
-      return new ProjectConfig(projectName);
+      return new ProjectConfig(projectName, getBaseConfig(sitePaths, allProjects, projectName));
     }
 
     public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException {
@@ -191,6 +217,8 @@
     }
   }
 
+  private final StoredConfig baseConfig;
+
   private Project project;
   private AccountsSection accountsSection;
   private GroupList groupList;
@@ -253,8 +281,9 @@
     commentLinkSections.add(commentLink);
   }
 
-  private ProjectConfig(Project.NameKey projectName) {
+  private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) {
     this.projectName = projectName;
+    this.baseConfig = baseConfig;
   }
 
   public void load(Repository repo) throws IOException, ConfigInvalidException {
@@ -516,11 +545,14 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
     readGroupList();
     groupsByName = mapGroupReferences();
 
     rulesId = getObjectId("rules.pl");
-    Config rc = readConfig(PROJECT_CONFIG);
+    Config rc = readConfig(PROJECT_CONFIG, baseConfig);
     project = new Project(projectName);
 
     Project p = project;
diff --git a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index c25b846..483e363 100644
--- a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
+++ b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -17,12 +17,17 @@
 import static com.google.gerrit.server.project.ProjectConfig.ACCESS;
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -31,22 +36,38 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.StoredConfig;
 
 public class ProjectConfigSchemaUpdate extends VersionedMetaData {
+  public static class Factory {
+    private final SitePaths sitePaths;
+    private final AllProjectsName allProjectsName;
+
+    @Inject
+    Factory(SitePaths sitePaths, AllProjectsName allProjectsName) {
+      this.sitePaths = sitePaths;
+      this.allProjectsName = allProjectsName;
+    }
+
+    ProjectConfigSchemaUpdate read(MetaDataUpdate update)
+        throws IOException, ConfigInvalidException {
+      ProjectConfigSchemaUpdate r =
+          new ProjectConfigSchemaUpdate(
+              update,
+              ProjectConfig.Factory.getBaseConfig(sitePaths, allProjectsName, allProjectsName));
+      r.load(update);
+      return r;
+    }
+  }
 
   private final MetaDataUpdate update;
+  @Nullable private final StoredConfig baseConfig;
   private Config config;
   private boolean updated;
 
-  public static ProjectConfigSchemaUpdate read(MetaDataUpdate update)
-      throws IOException, ConfigInvalidException {
-    ProjectConfigSchemaUpdate r = new ProjectConfigSchemaUpdate(update);
-    r.load(update);
-    return r;
-  }
-
-  private ProjectConfigSchemaUpdate(MetaDataUpdate update) {
+  private ProjectConfigSchemaUpdate(MetaDataUpdate update, @Nullable StoredConfig baseConfig) {
     this.update = update;
+    this.baseConfig = baseConfig;
   }
 
   @Override
@@ -56,7 +77,15 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    config = readConfig(ProjectConfig.PROJECT_CONFIG);
+    if (baseConfig != null) {
+      baseConfig.load();
+    }
+    config = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
+  }
+
+  @VisibleForTesting
+  Config getConfig() {
+    return config;
   }
 
   public void removeForceFromPermission(String name) {
diff --git a/java/com/google/gerrit/server/schema/Schema_130.java b/java/com/google/gerrit/server/schema/Schema_130.java
index 0c9d79d..e550121 100644
--- a/java/com/google/gerrit/server/schema/Schema_130.java
+++ b/java/com/google/gerrit/server/schema/Schema_130.java
@@ -41,15 +41,18 @@
 
   private final GitRepositoryManager repoManager;
   private final PersonIdent serverUser;
+  private final ProjectConfigSchemaUpdate.Factory projectConfigSchemaUpdateFactory;
 
   @Inject
   Schema_130(
       Provider<Schema_129> prior,
       GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverUser) {
+      @GerritPersonIdent PersonIdent serverUser,
+      ProjectConfigSchemaUpdate.Factory projectConfigSchemaUpdateFactory) {
     super(prior);
     this.repoManager = repoManager;
     this.serverUser = serverUser;
+    this.projectConfigSchemaUpdateFactory = projectConfigSchemaUpdateFactory;
   }
 
   @Override
@@ -60,7 +63,7 @@
     for (Project.NameKey projectName : repoList) {
       try (Repository git = repoManager.openRepository(projectName);
           MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, git)) {
-        ProjectConfigSchemaUpdate cfg = ProjectConfigSchemaUpdate.read(md);
+        ProjectConfigSchemaUpdate cfg = projectConfigSchemaUpdateFactory.read(md);
         cfg.removeForceFromPermission("pushTag");
         if (cfg.isUpdated()) {
           repoUpgraded.add(projectName);
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index 01ebb0e..8e3b71d 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -12,6 +12,7 @@
         "//java/com/google/gerrit/pgm/init/api",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/securestore/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:junit",
         "//lib/easymock",
diff --git a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
new file mode 100644
index 0000000..68b4a8e
--- /dev/null
+++ b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.securestore.testing.InMemorySecureStore;
+import com.google.gerrit.testing.TempFileUtil;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+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.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AllProjectsConfigTest {
+  private static final String ALL_PROJECTS = "All-The-Projects";
+
+  private SitePaths sitePaths;
+  private AllProjectsConfig allProjectsConfig;
+  private File allProjectsRepoFile;
+
+  @Before
+  public void setUp() throws Exception {
+    sitePaths = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+
+    Path gitPath = sitePaths.resolve("git");
+
+    StoredConfig gerritConfig =
+        new FileBasedConfig(
+            sitePaths.resolve("etc").resolve("gerrit.config").toFile(), FS.DETECTED);
+    gerritConfig.load();
+    gerritConfig.setString("gerrit", null, "basePath", gitPath.toAbsolutePath().toString());
+    gerritConfig.setString("gerrit", null, "allProjects", ALL_PROJECTS);
+    gerritConfig.save();
+
+    Files.createDirectories(sitePaths.resolve("git"));
+    allProjectsRepoFile = gitPath.resolve("All-The-Projects.git").toFile();
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      repo.create(true);
+    }
+
+    InMemorySecureStore secureStore = new InMemorySecureStore();
+    InitFlags flags = new InitFlags(sitePaths, secureStore, ImmutableList.of(), false);
+    ConsoleUI ui = createStrictMock(ConsoleUI.class);
+    replay(ui);
+    Section.Factory sections =
+        (name, subsection) -> new Section(flags, sitePaths, secureStore, ui, name, subsection);
+    allProjectsConfig =
+        new AllProjectsConfig(new AllProjectsNameOnInitProvider(sections), sitePaths, flags);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    TempFileUtil.cleanup();
+  }
+
+  @Test
+  public void noBaseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  @Test
+  public void baseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    Path baseConfigPath = sitePaths.etc_dir.resolve(ALL_PROJECTS).resolve("project.config");
+    Files.createDirectories(baseConfigPath.getParent());
+    Files.write(baseConfigPath, ImmutableList.of("[foo]", "bar = base"));
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("base");
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  private Config getConfig() throws IOException, ConfigInvalidException {
+    return allProjectsConfig.load().getConfig();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 764d49a..a1e0566 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.REQUIRE_CHANGE_ID;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
@@ -24,16 +26,22 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 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.config.PluginConfig;
+import com.google.gerrit.server.config.SitePaths;
 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.testing.GerritBaseTests;
+import com.google.gerrit.testing.TempFileUtil;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
@@ -50,6 +58,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -74,21 +83,31 @@
           + !LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE
           + "\n";
 
+  private static final AllProjectsName ALL_PROJECTS = new AllProjectsName("All-The-Projects");
+
   private final GroupReference developers =
       new GroupReference(new AccountGroup.UUID("X"), "Developers");
   private final GroupReference staff = new GroupReference(new AccountGroup.UUID("Y"), "Staff");
 
+  private SitePaths sitePaths;
   private ProjectConfig.Factory factory;
   private Repository db;
   private TestRepository<?> tr;
 
   @Before
   public void setUp() throws Exception {
-    factory = new ProjectConfig.Factory();
+    sitePaths = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+    factory = new ProjectConfig.Factory(sitePaths, ALL_PROJECTS);
     db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
     tr = new TestRepository<>(db);
   }
 
+  @After
+  public void tearDown() throws Exception {
+    TempFileUtil.cleanup();
+  }
+
   @Test
   public void readConfig() throws Exception {
     RevCommit rev =
@@ -593,6 +612,44 @@
                     + "commentlink.bugzilla must have either link or html"));
   }
 
+  @Test
+  public void readAllProjectsBaseConfigFromSitePaths() throws Exception {
+    ProjectConfig cfg = factory.create(ALL_PROJECTS);
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+
+    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.FALSE);
+  }
+
+  @Test
+  public void readOtherProjectIgnoresAllProjectsBaseConfig() throws Exception {
+    ProjectConfig cfg = factory.create(new Project.NameKey("test"));
+    cfg.load(db);
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+
+    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+
+    cfg.load(db);
+    // If we went through ProjectState, then this would return FALSE, since project.config for
+    // All-Projects would inherit from all_projects.config, and this project would inherit from
+    // All-Projects. But in ProjectConfig itself, there is no inheritance from All-Projects, so this
+    // continues to return the default.
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+        .isEqualTo(InheritableBoolean.INHERIT);
+  }
+
+  private Path writeDefaultAllProjectsConfig(String... lines) throws IOException {
+    Path dir = sitePaths.etc_dir.resolve(ALL_PROJECTS.get());
+    Files.createDirectories(dir);
+    return Files.write(dir.resolve("project.config"), ImmutableList.copyOf(lines));
+  }
+
   private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException {
     ProjectConfig cfg = factory.create(new Project.NameKey("test"));
     cfg.load(db, rev);
diff --git a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
new file mode 100644
index 0000000..de825e6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.testing.TempFileUtil;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+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.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ProjectConfigSchemaUpdateTest {
+  private static final String ALL_PROJECTS = "All-The-Projects";
+
+  private SitePaths sitePaths;
+  private ProjectConfigSchemaUpdate.Factory factory;
+  private File allProjectsRepoFile;
+
+  @Before
+  public void setUp() throws Exception {
+    sitePaths = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    Files.createDirectories(sitePaths.etc_dir);
+
+    Path gitPath = sitePaths.resolve("git");
+
+    StoredConfig gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
+    gerritConfig.load();
+    gerritConfig.setString("gerrit", null, "basePath", gitPath.toAbsolutePath().toString());
+    gerritConfig.setString("gerrit", null, "allProjects", ALL_PROJECTS);
+    gerritConfig.save();
+
+    Files.createDirectories(sitePaths.resolve("git"));
+    allProjectsRepoFile = gitPath.resolve("All-The-Projects.git").toFile();
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      repo.create(true);
+    }
+
+    factory = new ProjectConfigSchemaUpdate.Factory(sitePaths, new AllProjectsName(ALL_PROJECTS));
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    TempFileUtil.cleanup();
+  }
+
+  @Test
+  public void noBaseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  @Test
+  public void baseConfig() throws Exception {
+    assertThat(getConfig().getString("foo", null, "bar")).isNull();
+
+    Path baseConfigPath = sitePaths.etc_dir.resolve(ALL_PROJECTS).resolve("project.config");
+    Files.createDirectories(baseConfigPath.getParent());
+    Files.write(baseConfigPath, ImmutableList.of("[foo]", "bar = base"));
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("base");
+
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      tr.branch("refs/meta/config").commit().add("project.config", "[foo]\nbar = baz").create();
+    }
+
+    assertThat(getConfig().getString("foo", null, "bar")).isEqualTo("baz");
+  }
+
+  private Config getConfig() throws Exception {
+    try (Repository repo = new FileRepository(allProjectsRepoFile)) {
+      return factory
+          .read(
+              new MetaDataUpdate(
+                  GitReferenceUpdated.DISABLED, new Project.NameKey(ALL_PROJECTS), repo, null))
+          .getConfig();
+    }
+  }
+}