Merge "Add BooleanProjectConfig to reduce boiler plate"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index dda1e6c..f54f012 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -69,6 +69,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -874,7 +875,7 @@
   protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = ProjectConfig.read(md);
-      config.getProject().setUseContributorAgreements(value);
+      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value);
       config.commit(md);
       projectCache.evict(config.getProject());
     }
@@ -883,7 +884,7 @@
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = ProjectConfig.read(md);
-      config.getProject().setUseSignedOffBy(value);
+      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, value);
       config.commit(md);
       projectCache.evict(config.getProject());
     }
@@ -1558,7 +1559,10 @@
 
   protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    config
+        .getProject()
+        .setBooleanConfig(
+            BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
     saveProjectConfig(project, config);
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 443e2064..7fa65cf 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -23,6 +23,7 @@
 
 public class ConfigInfo {
   public String description;
+
   public InheritedBooleanInfo useContributorAgreements;
   public InheritedBooleanInfo useContentMerge;
   public InheritedBooleanInfo useSignedOffBy;
@@ -34,6 +35,7 @@
   public InheritedBooleanInfo privateByDefault;
   public InheritedBooleanInfo enableReviewerByEmail;
   public InheritedBooleanInfo matchAuthorToCommitterDate;
+
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public ProjectState state;
diff --git a/java/com/google/gerrit/gpg/SignedPushModule.java b/java/com/google/gerrit/gpg/SignedPushModule.java
index c32e1df..c420f6f 100644
--- a/java/com/google/gerrit/gpg/SignedPushModule.java
+++ b/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.config.AllUsersName;
@@ -88,7 +89,7 @@
     @Override
     public void init(Project.NameKey project, ReceivePack rp) {
       ProjectState ps = projectCache.get(project);
-      if (!ps.isEnableSignedPush()) {
+      if (!ps.is(BooleanProjectConfig.ENABLE_SIGNED_PUSH)) {
         rp.setSignedPushConfig(null);
         return;
       } else if (signedPushConfig == null) {
@@ -103,7 +104,7 @@
       rp.setSignedPushConfig(signedPushConfig);
 
       List<PreReceiveHook> hooks = new ArrayList<>(3);
-      if (ps.isRequireSignedPush()) {
+      if (ps.is(BooleanProjectConfig.REQUIRE_SIGNED_PUSH)) {
         hooks.add(SignedPushPreReceiveHook.Required.INSTANCE);
       }
       hooks.add(hook);
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
new file mode 100644
index 0000000..ef8156b
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 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.reviewdb.client;
+
+/**
+ * Contains all inheritable boolean project configs and maps internal representations to API
+ * objects.
+ *
+ * <p>Perform the following steps for adding a new inheritable boolean project config:
+ *
+ * <ol>
+ *   <li>Add a field to {@link com.google.gerrit.extensions.api.projects.ConfigInput}
+ *   <li>Add a field to {@link com.google.gerrit.extensions.api.projects.ConfigInfo}
+ *   <li>Add the config to this enum
+ *   <li>Add API mappers to {@link
+ *       com.google.gerrit.server.project.BooleanProjectConfigTransformations}
+ * </ol>
+ */
+public enum BooleanProjectConfig {
+  USE_CONTRIBUTOR_AGREEMENTS("receive", "requireContributorAgreement"),
+  USE_SIGNED_OFF_BY("receive", "requireSignedOffBy"),
+  USE_CONTENT_MERGE("submit", "mergeContent"),
+  REQUIRE_CHANGE_ID("receive", "requireChangeId"),
+  CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET("receive", "createNewChangeForAllNotInTarget"),
+  ENABLE_SIGNED_PUSH("receive", "enableSignedPush"),
+  REQUIRE_SIGNED_PUSH("receive", "requireSignedPush"),
+  REJECT_IMPLICIT_MERGES("receive", "rejectImplicitMerges"),
+  PRIVATE_BY_DEFAULT("change", "privateByDefault"),
+  ENABLE_REVIEWER_BY_EMAIL("reviewer", "enableByEmail"),
+  MATCH_AUTHOR_TO_COMMITTER_DATE("project", "matchAuthorToCommitterDate");
+
+  // Git config
+  private final String section;
+  private final String name;
+
+  BooleanProjectConfig(String section, String name) {
+    this.section = section;
+    this.name = name;
+  }
+
+  public String getSection() {
+    return section;
+  }
+
+  public String getSubSection() {
+    return null;
+  }
+
+  public String getName() {
+    return name;
+  }
+}
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index e756ce5..d656b68 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 /** Projects match a source code repository managed by Gerrit */
 public final class Project {
@@ -74,9 +77,7 @@
 
   protected String description;
 
-  protected InheritableBoolean useContributorAgreements;
-
-  protected InheritableBoolean useSignedOffBy;
+  protected Map<BooleanProjectConfig, InheritableBoolean> booleanConfigs;
 
   protected SubmitType submitType;
 
@@ -84,46 +85,24 @@
 
   protected NameKey parent;
 
-  protected InheritableBoolean requireChangeID;
-
   protected String maxObjectSizeLimit;
 
-  protected InheritableBoolean useContentMerge;
-
   protected String defaultDashboardId;
 
   protected String localDefaultDashboardId;
 
   protected String themeName;
 
-  protected InheritableBoolean createNewChangeForAllNotInTarget;
-
-  protected InheritableBoolean enableSignedPush;
-  protected InheritableBoolean requireSignedPush;
-
-  protected InheritableBoolean rejectImplicitMerges;
-  protected InheritableBoolean privateByDefault;
-
-  protected InheritableBoolean enableReviewerByEmail;
-
-  protected InheritableBoolean matchAuthorToCommitterDate;
-
   protected Project() {}
 
   public Project(Project.NameKey nameKey) {
     name = nameKey;
     submitType = SubmitType.MERGE_IF_NECESSARY;
     state = ProjectState.ACTIVE;
-    useContributorAgreements = InheritableBoolean.INHERIT;
-    useSignedOffBy = InheritableBoolean.INHERIT;
-    requireChangeID = InheritableBoolean.INHERIT;
-    useContentMerge = InheritableBoolean.INHERIT;
-    createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
-    enableSignedPush = InheritableBoolean.INHERIT;
-    requireSignedPush = InheritableBoolean.INHERIT;
-    privateByDefault = InheritableBoolean.INHERIT;
-    enableReviewerByEmail = InheritableBoolean.INHERIT;
-    matchAuthorToCommitterDate = InheritableBoolean.INHERIT;
+
+    booleanConfigs = new HashMap<>();
+    Arrays.stream(BooleanProjectConfig.values())
+        .forEach(c -> booleanConfigs.put(c, InheritableBoolean.INHERIT));
   }
 
   public Project.NameKey getNameKey() {
@@ -142,102 +121,22 @@
     description = d;
   }
 
-  public InheritableBoolean getUseContributorAgreements() {
-    return useContributorAgreements;
-  }
-
-  public InheritableBoolean getUseSignedOffBy() {
-    return useSignedOffBy;
-  }
-
-  public InheritableBoolean getUseContentMerge() {
-    return useContentMerge;
-  }
-
-  public InheritableBoolean getRequireChangeID() {
-    return requireChangeID;
-  }
-
   public String getMaxObjectSizeLimit() {
     return maxObjectSizeLimit;
   }
 
-  public InheritableBoolean getRejectImplicitMerges() {
-    return rejectImplicitMerges;
+  public InheritableBoolean getBooleanConfig(BooleanProjectConfig config) {
+    return booleanConfigs.get(config);
   }
 
-  public InheritableBoolean getPrivateByDefault() {
-    return privateByDefault;
-  }
-
-  public void setPrivateByDefault(InheritableBoolean privateByDefault) {
-    this.privateByDefault = privateByDefault;
-  }
-
-  public InheritableBoolean getEnableReviewerByEmail() {
-    return enableReviewerByEmail;
-  }
-
-  public void setEnableReviewerByEmail(InheritableBoolean enable) {
-    enableReviewerByEmail = enable;
-  }
-
-  public InheritableBoolean getMatchAuthorToCommitterDate() {
-    return matchAuthorToCommitterDate;
-  }
-
-  public void setMatchAuthorToCommitterDate(InheritableBoolean match) {
-    matchAuthorToCommitterDate = match;
-  }
-
-  public void setUseContributorAgreements(InheritableBoolean u) {
-    useContributorAgreements = u;
-  }
-
-  public void setUseSignedOffBy(InheritableBoolean sbo) {
-    useSignedOffBy = sbo;
-  }
-
-  public void setUseContentMerge(InheritableBoolean cm) {
-    useContentMerge = cm;
-  }
-
-  public void setRequireChangeID(InheritableBoolean cid) {
-    requireChangeID = cid;
-  }
-
-  public InheritableBoolean getCreateNewChangeForAllNotInTarget() {
-    return createNewChangeForAllNotInTarget;
-  }
-
-  public void setCreateNewChangeForAllNotInTarget(InheritableBoolean useAllNotInTarget) {
-    this.createNewChangeForAllNotInTarget = useAllNotInTarget;
-  }
-
-  public InheritableBoolean getEnableSignedPush() {
-    return enableSignedPush;
-  }
-
-  public void setEnableSignedPush(InheritableBoolean enable) {
-    enableSignedPush = enable;
-  }
-
-  public InheritableBoolean getRequireSignedPush() {
-    return requireSignedPush;
-  }
-
-  public void setRequireSignedPush(InheritableBoolean require) {
-    requireSignedPush = require;
+  public void setBooleanConfig(BooleanProjectConfig config, InheritableBoolean val) {
+    booleanConfigs.replace(config, val);
   }
 
   public void setMaxObjectSizeLimit(String limit) {
     maxObjectSizeLimit = limit;
   }
 
-  public void setRejectImplicitMerges(InheritableBoolean check) {
-    rejectImplicitMerges = check;
-  }
-
   public SubmitType getSubmitType() {
     return submitType;
   }
@@ -280,14 +179,10 @@
 
   public void copySettingsFrom(Project update) {
     description = update.description;
-    useContributorAgreements = update.useContributorAgreements;
-    useSignedOffBy = update.useSignedOffBy;
-    useContentMerge = update.useContentMerge;
-    requireChangeID = update.requireChangeID;
+    booleanConfigs = new HashMap<>(update.booleanConfigs);
     submitType = update.submitType;
     state = update.state;
     maxObjectSizeLimit = update.maxObjectSizeLimit;
-    createNewChangeForAllNotInTarget = update.createNewChangeForAllNotInTarget;
   }
 
   /**
diff --git a/java/com/google/gerrit/server/change/CreateChange.java b/java/com/google/gerrit/server/change/CreateChange.java
index 9aefa12..56ad000 100644
--- a/java/com/google/gerrit/server/change/CreateChange.java
+++ b/java/com/google/gerrit/server/change/CreateChange.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -253,7 +254,7 @@
         c = newCommit(oi, rw, author, mergeTip, commitMessage);
       }
 
-      boolean privateByDefault = rsrc.getProjectState().isPrivateByDefault();
+      boolean privateByDefault = rsrc.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
       Change.Id changeId = new Change.Id(seq.nextChangeId());
       ChangeInserter ins = changeInserterFactory.create(changeId, c, refName);
       ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
diff --git a/java/com/google/gerrit/server/change/PostReviewers.java b/java/com/google/gerrit/server/change/PostReviewers.java
index 0995156..782980b 100644
--- a/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/change/PostReviewers.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -181,7 +182,10 @@
       return fail(reviewer, e.getMessage());
     }
     boolean confirmed = input.confirmed();
-    boolean allowByEmail = projectCache.checkedGet(rsrc.getProject()).isEnableReviewerByEmail();
+    boolean allowByEmail =
+        projectCache
+            .checkedGet(rsrc.getProject())
+            .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
 
     Addition byAccountId =
         addByAccountId(reviewer, rsrc, state, notify, accountsToNotify, allowGroup, allowByEmail);
diff --git a/java/com/google/gerrit/server/change/PutMessage.java b/java/com/google/gerrit/server/change/PutMessage.java
index a577004..419d789 100644
--- a/java/com/google/gerrit/server/change/PutMessage.java
+++ b/java/com/google/gerrit/server/change/PutMessage.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
@@ -114,7 +115,7 @@
 
     ensureCanEditCommitMessage(resource.getNotes());
     ensureChangeIdIsCorrect(
-        projectCache.checkedGet(resource.getProject()).isRequireChangeID(),
+        projectCache.checkedGet(resource.getProject()).is(BooleanProjectConfig.REQUIRE_CHANGE_ID),
         resource.getChange().getKey().get(),
         sanitizedCommitMessage);
 
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 9ea9dcb..f0712f3 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.LabelId;
@@ -168,7 +169,7 @@
         approvalsUtil,
         project,
         commitMessageGenerator,
-        project.isUseContentMerge());
+        project.is(BooleanProjectConfig.USE_CONTENT_MERGE));
   }
 
   @AssistedInject
@@ -869,7 +870,7 @@
   }
 
   private static void matchAuthorToCommitterDate(ProjectState project, CommitBuilder commit) {
-    if (project.isMatchAuthorToCommitterDate()) {
+    if (project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE)) {
       commit.setAuthor(
           new PersonIdent(
               commit.getAuthor(),
diff --git a/java/com/google/gerrit/server/git/ProjectConfig.java b/java/com/google/gerrit/server/git/ProjectConfig.java
index 3086e81..e5b4a17e 100644
--- a/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -88,8 +89,6 @@
 
   private static final String PROJECT = "project";
   private static final String KEY_DESCRIPTION = "description";
-  private static final String KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE =
-      "matchAuthorToCommitterDate";
 
   public static final String ACCESS = "access";
   private static final String KEY_INHERIT_FROM = "inheritFrom";
@@ -115,24 +114,14 @@
   private static final String CAPABILITY = "capability";
 
   private static final String RECEIVE = "receive";
-  private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
-  private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
-  private static final String KEY_USE_ALL_NOT_IN_TARGET = "createNewChangeForAllNotInTarget";
-  private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
-  private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT = "requireContributorAgreement";
   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
-  private static final String KEY_ENABLE_SIGNED_PUSH = "enableSignedPush";
-  private static final String KEY_REQUIRE_SIGNED_PUSH = "requireSignedPush";
-  private static final String KEY_REJECT_IMPLICIT_MERGES = "rejectImplicitMerges";
-
-  private static final String CHANGE = "change";
-  private static final String KEY_PRIVATE_BY_DEFAULT = "privateByDefault";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
-  private static final String KEY_MERGE_CONTENT = "mergeContent";
   private static final String KEY_STATE = "state";
 
+  private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
+
   private static final String SUBSCRIBE_SECTION = "allowSuperproject";
   private static final String SUBSCRIBE_MATCH_REFS = "matching";
   private static final String SUBSCRIBE_MULTI_MATCH_REFS = "all";
@@ -157,9 +146,6 @@
   private static final String KEY_CAN_OVERRIDE = "canOverride";
   private static final String KEY_BRANCH = "branch";
 
-  private static final String REVIEWER = "reviewer";
-  private static final String KEY_ENABLE_REVIEWER_BY_EMAIL = "enableByEmail";
-
   private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
   private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";
 
@@ -520,37 +506,20 @@
     }
     p.setParentName(rc.getString(ACCESS, null, KEY_INHERIT_FROM));
 
-    p.setUseContributorAgreements(
-        getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, InheritableBoolean.INHERIT));
-    p.setUseSignedOffBy(
-        getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, InheritableBoolean.INHERIT));
-    p.setRequireChangeID(
-        getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, InheritableBoolean.INHERIT));
-    p.setCreateNewChangeForAllNotInTarget(
-        getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
-    p.setEnableSignedPush(
-        getEnum(rc, RECEIVE, null, KEY_ENABLE_SIGNED_PUSH, InheritableBoolean.INHERIT));
-    p.setRequireSignedPush(
-        getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_PUSH, InheritableBoolean.INHERIT));
+    for (BooleanProjectConfig config : BooleanProjectConfig.values()) {
+      p.setBooleanConfig(
+          config,
+          getEnum(
+              rc,
+              config.getSection(),
+              config.getSubSection(),
+              config.getName(),
+              InheritableBoolean.INHERIT));
+    }
+
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
-    p.setRejectImplicitMerges(
-        getEnum(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, InheritableBoolean.INHERIT));
-
-    p.setPrivateByDefault(
-        getEnum(rc, CHANGE, null, KEY_PRIVATE_BY_DEFAULT, InheritableBoolean.INHERIT));
-
-    p.setEnableReviewerByEmail(
-        getEnum(rc, REVIEWER, null, KEY_ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.INHERIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_ACTION));
-    p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, InheritableBoolean.INHERIT));
-    p.setMatchAuthorToCommitterDate(
-        getEnum(
-            rc,
-            SUBMIT,
-            null,
-            KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE,
-            InheritableBoolean.INHERIT));
     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE));
 
     p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
@@ -1056,87 +1025,25 @@
       rc.unset(PROJECT, null, KEY_DESCRIPTION);
     }
     set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName());
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_REQUIRE_CONTRIBUTOR_AGREEMENT,
-        p.getUseContributorAgreements(),
-        InheritableBoolean.INHERIT);
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_REQUIRE_SIGNED_OFF_BY,
-        p.getUseSignedOffBy(),
-        InheritableBoolean.INHERIT);
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_REQUIRE_CHANGE_ID,
-        p.getRequireChangeID(),
-        InheritableBoolean.INHERIT);
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_USE_ALL_NOT_IN_TARGET,
-        p.getCreateNewChangeForAllNotInTarget(),
-        InheritableBoolean.INHERIT);
+
+    for (BooleanProjectConfig config : BooleanProjectConfig.values()) {
+      set(
+          rc,
+          config.getSection(),
+          config.getSubSection(),
+          config.getName(),
+          p.getBooleanConfig(config),
+          InheritableBoolean.INHERIT);
+    }
+
     set(
         rc,
         RECEIVE,
         null,
         KEY_MAX_OBJECT_SIZE_LIMIT,
         validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_ENABLE_SIGNED_PUSH,
-        p.getEnableSignedPush(),
-        InheritableBoolean.INHERIT);
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_REQUIRE_SIGNED_PUSH,
-        p.getRequireSignedPush(),
-        InheritableBoolean.INHERIT);
-    set(
-        rc,
-        RECEIVE,
-        null,
-        KEY_REJECT_IMPLICIT_MERGES,
-        p.getRejectImplicitMerges(),
-        InheritableBoolean.INHERIT);
-
-    set(
-        rc,
-        CHANGE,
-        null,
-        KEY_PRIVATE_BY_DEFAULT,
-        p.getPrivateByDefault(),
-        InheritableBoolean.INHERIT);
-
-    set(
-        rc,
-        REVIEWER,
-        null,
-        KEY_ENABLE_REVIEWER_BY_EMAIL,
-        p.getEnableReviewerByEmail(),
-        InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_ACTION);
-    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
-    set(
-        rc,
-        SUBMIT,
-        null,
-        KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE,
-        p.getMatchAuthorToCommitterDate(),
-        InheritableBoolean.INHERIT);
 
     set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE);
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 8efd0c0..dcbbea8 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -75,6 +75,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -486,7 +487,8 @@
     newChanges = Collections.emptyList();
 
     // Other settings populated during processing.
-    newChangeForAllNotInTarget = projectState.isCreateNewChangeForAllNotInTarget();
+    newChangeForAllNotInTarget =
+        projectState.is(BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET);
 
     // Handles for outputting back over the wire to the end user.
     messageSender = new ReceivePackMessageSender();
@@ -1735,7 +1737,9 @@
       int alreadyTracked = 0;
       boolean rejectImplicitMerges =
           start.getParentCount() == 1
-              && projectCache.get(project.getNameKey()).isRejectImplicitMerges()
+              && projectCache
+                  .get(project.getNameKey())
+                  .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES)
               // Don't worry about implicit merges when creating changes for
               // already-merged commits; they're already in history, so it's too
               // late.
@@ -2124,7 +2128,8 @@
     }
 
     private void setChangeId(int id) {
-      boolean privateByDefault = projectCache.get(project.getNameKey()).isPrivateByDefault();
+      boolean privateByDefault =
+          projectCache.get(project.getNameKey()).is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
 
       changeId = new Change.Id(id);
       ins =
diff --git a/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index 5421254..a3e4e16 100644
--- a/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.RebaseChangeOp;
@@ -179,7 +180,8 @@
                 // Do not post message after inserting new patchset because there
                 // will be one about change being merged already.
                 .setPostMessage(false)
-                .setMatchAuthorToCommitterDate(args.project.isMatchAuthorToCommitterDate());
+                .setMatchAuthorToCommitterDate(
+                    args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE));
         try {
           rebaseOp.updateRepo(ctx);
         } catch (MergeConflictException | NoSuchChangeException e) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 9bd70c0..9a29193 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -257,7 +258,7 @@
       String sha1 = commit.abbreviate(RevId.ABBREV_LEN).name();
 
       if (idList.isEmpty()) {
-        if (projectState.isRequireChangeID()) {
+        if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
           String shortMsg = commit.getShortMessage();
           if (shortMsg.startsWith(CHANGE_ID_PREFIX)
               && CHANGE_ID
@@ -516,7 +517,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      if (!state.isUseSignedOffBy()) {
+      if (!state.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
         return Collections.emptyList();
       }
 
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
new file mode 100644
index 0000000..1f94025
--- /dev/null
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.api.projects.ConfigInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInfo.InheritedBooleanInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import java.util.Arrays;
+import java.util.HashSet;
+
+/** Provides transformations to get and set BooleanProjectConfigs from the API. */
+public class BooleanProjectConfigTransformations {
+
+  private static ImmutableMap<BooleanProjectConfig, Mapper> MAPPER =
+      ImmutableMap.<BooleanProjectConfig, Mapper>builder()
+          .put(
+              BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS,
+              new Mapper(i -> i.useContributorAgreements, (i, v) -> i.useContributorAgreements = v))
+          .put(
+              BooleanProjectConfig.USE_SIGNED_OFF_BY,
+              new Mapper(i -> i.useSignedOffBy, (i, v) -> i.useSignedOffBy = v))
+          .put(
+              BooleanProjectConfig.USE_CONTENT_MERGE,
+              new Mapper(i -> i.useContentMerge, (i, v) -> i.useContentMerge = v))
+          .put(
+              BooleanProjectConfig.REQUIRE_CHANGE_ID,
+              new Mapper(i -> i.requireChangeId, (i, v) -> i.requireChangeId = v))
+          .put(
+              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+              new Mapper(
+                  i -> i.createNewChangeForAllNotInTarget,
+                  (i, v) -> i.createNewChangeForAllNotInTarget = v))
+          .put(
+              BooleanProjectConfig.ENABLE_SIGNED_PUSH,
+              new Mapper(i -> i.enableSignedPush, (i, v) -> i.enableSignedPush = v))
+          .put(
+              BooleanProjectConfig.REQUIRE_SIGNED_PUSH,
+              new Mapper(i -> i.requireSignedPush, (i, v) -> i.requireSignedPush = v))
+          .put(
+              BooleanProjectConfig.REJECT_IMPLICIT_MERGES,
+              new Mapper(i -> i.rejectImplicitMerges, (i, v) -> i.rejectImplicitMerges = v))
+          .put(
+              BooleanProjectConfig.PRIVATE_BY_DEFAULT,
+              new Mapper(i -> i.privateByDefault, (i, v) -> i.privateByDefault = v))
+          .put(
+              BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL,
+              new Mapper(i -> i.enableReviewerByEmail, (i, v) -> i.enableReviewerByEmail = v))
+          .put(
+              BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE,
+              new Mapper(
+                  i -> i.matchAuthorToCommitterDate, (i, v) -> i.matchAuthorToCommitterDate = v))
+          .build();
+
+  static {
+    // Verify that each BooleanProjectConfig has to/from API mappers in BooleanProjectConfigTransformations
+    if (!Sets.symmetricDifference(
+            MAPPER.keySet(), new HashSet<>(Arrays.asList(BooleanProjectConfig.values())))
+        .isEmpty()) {
+      throw new IllegalStateException(
+          "All values of BooleanProjectConfig must have transformations associated with them");
+    }
+  }
+
+  @FunctionalInterface
+  private interface ToApi {
+    void apply(ConfigInfo info, InheritedBooleanInfo val);
+  }
+
+  @FunctionalInterface
+  private interface FromApi {
+    InheritableBoolean apply(ConfigInput input);
+  }
+
+  public static void set(BooleanProjectConfig cfg, ConfigInfo info, InheritedBooleanInfo val) {
+    MAPPER.get(cfg).set(info, val);
+  }
+
+  public static InheritableBoolean get(BooleanProjectConfig cfg, ConfigInput input) {
+    return MAPPER.get(cfg).get(input);
+  }
+
+  private static class Mapper {
+    private final FromApi fromApi;
+    private final ToApi toApi;
+
+    private Mapper(FromApi fromApi, ToApi toApi) {
+      this.fromApi = fromApi;
+      this.toApi = toApi;
+    }
+
+    public void set(ConfigInfo info, InheritedBooleanInfo val) {
+      toApi.apply(info, val);
+    }
+
+    public InheritableBoolean get(ConfigInput input) {
+      return fromApi.apply(input);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index db0787c..943f606 100644
--- a/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -51,65 +52,20 @@
     Project p = projectState.getProject();
     this.description = Strings.emptyToNull(p.getDescription());
 
-    InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
-    InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
-    InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
-    InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
-    InheritedBooleanInfo createNewChangeForAllNotInTarget = new InheritedBooleanInfo();
-    InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
-    InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
-    InheritedBooleanInfo rejectImplicitMerges = new InheritedBooleanInfo();
-    InheritedBooleanInfo privateByDefault = new InheritedBooleanInfo();
-    InheritedBooleanInfo enableReviewerByEmail = new InheritedBooleanInfo();
-    InheritedBooleanInfo matchAuthorToCommitterDate = new InheritedBooleanInfo();
-
-    useContributorAgreements.value = projectState.isUseContributorAgreements();
-    useSignedOffBy.value = projectState.isUseSignedOffBy();
-    useContentMerge.value = projectState.isUseContentMerge();
-    requireChangeId.value = projectState.isRequireChangeID();
-    createNewChangeForAllNotInTarget.value = projectState.isCreateNewChangeForAllNotInTarget();
-
-    useContributorAgreements.configuredValue = p.getUseContributorAgreements();
-    useSignedOffBy.configuredValue = p.getUseSignedOffBy();
-    useContentMerge.configuredValue = p.getUseContentMerge();
-    requireChangeId.configuredValue = p.getRequireChangeID();
-    createNewChangeForAllNotInTarget.configuredValue = p.getCreateNewChangeForAllNotInTarget();
-    enableSignedPush.configuredValue = p.getEnableSignedPush();
-    requireSignedPush.configuredValue = p.getRequireSignedPush();
-    rejectImplicitMerges.configuredValue = p.getRejectImplicitMerges();
-    privateByDefault.configuredValue = p.getPrivateByDefault();
-    enableReviewerByEmail.configuredValue = p.getEnableReviewerByEmail();
-    matchAuthorToCommitterDate.configuredValue = p.getMatchAuthorToCommitterDate();
-
     ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
-    if (parentState != null) {
-      useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
-      useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
-      useContentMerge.inheritedValue = parentState.isUseContentMerge();
-      requireChangeId.inheritedValue = parentState.isRequireChangeID();
-      createNewChangeForAllNotInTarget.inheritedValue =
-          parentState.isCreateNewChangeForAllNotInTarget();
-      enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
-      requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
-      privateByDefault.inheritedValue = projectState.isPrivateByDefault();
-      rejectImplicitMerges.inheritedValue = projectState.isRejectImplicitMerges();
-      enableReviewerByEmail.inheritedValue = projectState.isEnableReviewerByEmail();
-      matchAuthorToCommitterDate.inheritedValue = projectState.isMatchAuthorToCommitterDate();
+    for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
+      InheritedBooleanInfo info = new InheritedBooleanInfo();
+      info.configuredValue = p.getBooleanConfig(cfg);
+      if (parentState != null) {
+        info.inheritedValue = parentState.is(cfg);
+      }
+      BooleanProjectConfigTransformations.set(cfg, this, info);
     }
 
-    this.useContributorAgreements = useContributorAgreements;
-    this.useSignedOffBy = useSignedOffBy;
-    this.useContentMerge = useContentMerge;
-    this.requireChangeId = requireChangeId;
-    this.rejectImplicitMerges = rejectImplicitMerges;
-    this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
-    this.enableReviewerByEmail = enableReviewerByEmail;
-    this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
-    if (serverEnableSignedPush) {
-      this.enableSignedPush = enableSignedPush;
-      this.requireSignedPush = requireSignedPush;
+    if (!serverEnableSignedPush) {
+      this.enableSignedPush = null;
+      this.requireSignedPush = null;
     }
-    this.privateByDefault = privateByDefault;
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index 0033b12..2f96bd5 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -65,7 +66,7 @@
       throw new IOException("Can't load All-Projects");
     }
 
-    if (!projectState.isUseContributorAgreements()) {
+    if (!projectState.is(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS)) {
       return;
     }
 
diff --git a/java/com/google/gerrit/server/project/CreateProject.java b/java/com/google/gerrit/server/project/CreateProject.java
index 70cb266..6a4de1c 100644
--- a/java/com/google/gerrit/server/project/CreateProject.java
+++ b/java/com/google/gerrit/server/project/CreateProject.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -273,11 +274,14 @@
       newProject.setSubmitType(
           MoreObjects.firstNonNull(
               args.submitType, repositoryCfg.getDefaultSubmitType(args.getProject())));
-      newProject.setUseContributorAgreements(args.contributorAgreements);
-      newProject.setUseSignedOffBy(args.signedOffBy);
-      newProject.setUseContentMerge(args.contentMerge);
-      newProject.setCreateNewChangeForAllNotInTarget(args.newChangeForAllNotInTarget);
-      newProject.setRequireChangeID(args.changeIdRequired);
+      newProject.setBooleanConfig(
+          BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, args.contributorAgreements);
+      newProject.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, args.signedOffBy);
+      newProject.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, args.contentMerge);
+      newProject.setBooleanConfig(
+          BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+          args.newChangeForAllNotInTarget);
+      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
       newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
       if (args.newParent != null) {
         newProject.setParentName(args.newParent);
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 3c55baf..a189d92 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -31,8 +31,8 @@
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.ThemeInfo;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -66,7 +66,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -375,48 +374,19 @@
     return isAllUsers;
   }
 
-  public boolean isUseContributorAgreements() {
-    return getInheritableBoolean(Project::getUseContributorAgreements);
-  }
-
-  public boolean isUseContentMerge() {
-    return getInheritableBoolean(Project::getUseContentMerge);
-  }
-
-  public boolean isUseSignedOffBy() {
-    return getInheritableBoolean(Project::getUseSignedOffBy);
-  }
-
-  public boolean isRequireChangeID() {
-    return getInheritableBoolean(Project::getRequireChangeID);
-  }
-
-  public boolean isCreateNewChangeForAllNotInTarget() {
-    return getInheritableBoolean(Project::getCreateNewChangeForAllNotInTarget);
-  }
-
-  public boolean isEnableSignedPush() {
-    return getInheritableBoolean(Project::getEnableSignedPush);
-  }
-
-  public boolean isRequireSignedPush() {
-    return getInheritableBoolean(Project::getRequireSignedPush);
-  }
-
-  public boolean isRejectImplicitMerges() {
-    return getInheritableBoolean(Project::getRejectImplicitMerges);
-  }
-
-  public boolean isPrivateByDefault() {
-    return getInheritableBoolean(Project::getPrivateByDefault);
-  }
-
-  public boolean isEnableReviewerByEmail() {
-    return getInheritableBoolean(Project::getEnableReviewerByEmail);
-  }
-
-  public boolean isMatchAuthorToCommitterDate() {
-    return getInheritableBoolean(Project::getMatchAuthorToCommitterDate);
+  public boolean is(BooleanProjectConfig config) {
+    for (ProjectState s : tree()) {
+      switch (s.getProject().getBooleanConfig(config)) {
+        case TRUE:
+          return true;
+        case FALSE:
+          return false;
+        case INHERIT:
+        default:
+          continue;
+      }
+    }
+    return false;
   }
 
   /** All available label types. */
@@ -561,21 +531,6 @@
     return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
   }
 
-  private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
-    for (ProjectState s : tree()) {
-      switch (func.apply(s.getProject())) {
-        case TRUE:
-          return true;
-        case FALSE:
-          return false;
-        case INHERIT:
-        default:
-          continue;
-      }
-    }
-    return false;
-  }
-
   private LabelTypes loadLabelTypes() {
     Map<String, LabelType> types = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
diff --git a/java/com/google/gerrit/server/project/PutConfig.java b/java/com/google/gerrit/server/project/PutConfig.java
index 9dd8b43..0d74314 100644
--- a/java/com/google/gerrit/server/project/PutConfig.java
+++ b/java/com/google/gerrit/server/project/PutConfig.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -28,6 +29,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.EnableSignedPush;
@@ -120,39 +122,11 @@
 
       p.setDescription(Strings.emptyToNull(input.description));
 
-      if (input.useContributorAgreements != null) {
-        p.setUseContributorAgreements(input.useContributorAgreements);
-      }
-      if (input.useContentMerge != null) {
-        p.setUseContentMerge(input.useContentMerge);
-      }
-      if (input.useSignedOffBy != null) {
-        p.setUseSignedOffBy(input.useSignedOffBy);
-      }
-
-      if (input.createNewChangeForAllNotInTarget != null) {
-        p.setCreateNewChangeForAllNotInTarget(input.createNewChangeForAllNotInTarget);
-      }
-
-      if (input.requireChangeId != null) {
-        p.setRequireChangeID(input.requireChangeId);
-      }
-
-      if (serverEnableSignedPush) {
-        if (input.enableSignedPush != null) {
-          p.setEnableSignedPush(input.enableSignedPush);
+      for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
+        InheritableBoolean val = BooleanProjectConfigTransformations.get(cfg, input);
+        if (val != null) {
+          p.setBooleanConfig(cfg, val);
         }
-        if (input.requireSignedPush != null) {
-          p.setRequireSignedPush(input.requireSignedPush);
-        }
-      }
-
-      if (input.rejectImplicitMerges != null) {
-        p.setRejectImplicitMerges(input.rejectImplicitMerges);
-      }
-
-      if (input.privateByDefault != null) {
-        p.setPrivateByDefault(input.privateByDefault);
       }
 
       if (input.maxObjectSizeLimit != null) {
@@ -167,14 +141,6 @@
         p.setState(input.state);
       }
 
-      if (input.enableReviewerByEmail != null) {
-        p.setEnableReviewerByEmail(input.enableReviewerByEmail);
-      }
-
-      if (input.matchAuthorToCommitterDate != null) {
-        p.setMatchAuthorToCommitterDate(input.matchAuthorToCommitterDate);
-      }
-
       if (input.pluginConfigValues != null) {
         setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
       }
diff --git a/java/com/google/gerrit/server/project/RefControl.java b/java/com/google/gerrit/server/project/RefControl.java
index 9a23449..d674039 100644
--- a/java/com/google/gerrit/server/project/RefControl.java
+++ b/java/com/google/gerrit/server/project/RefControl.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -584,7 +585,7 @@
               && canForgeCommitter()
               && canForgeGerritServerIdentity()
               && canUploadMerges()
-              && !projectControl.getProjectState().isUseSignedOffBy();
+              && !projectControl.getProjectState().is(BooleanProjectConfig.USE_SIGNED_OFF_BY);
       }
       throw new PermissionBackendException(perm + " unsupported");
     }
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index dbcb879..02ea3f2 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -115,7 +116,10 @@
       ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
       ConflictKey conflictsKey =
           new ConflictKey(
-              changeDataCache.getTestAgainst(), other, str.type, projectState.isUseContentMerge());
+              changeDataCache.getTestAgainst(),
+              other,
+              str.type,
+              projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
       Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
       if (conflicts != null) {
         return conflicts;
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index cfa379f..62f8ad1 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -150,11 +151,11 @@
       ProjectConfig config = ProjectConfig.read(md);
       Project p = config.getProject();
       p.setDescription("Access inherited by all other projects.");
-      p.setRequireChangeID(InheritableBoolean.TRUE);
-      p.setUseContentMerge(InheritableBoolean.TRUE);
-      p.setUseContributorAgreements(InheritableBoolean.FALSE);
-      p.setUseSignedOffBy(InheritableBoolean.FALSE);
-      p.setEnableSignedPush(InheritableBoolean.FALSE);
+      p.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.TRUE);
+      p.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, InheritableBoolean.TRUE);
+      p.setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, InheritableBoolean.FALSE);
+      p.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, InheritableBoolean.FALSE);
+      p.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, InheritableBoolean.FALSE);
 
       AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
       AccessSection all = config.getAccessSection(AccessSection.ALL, true);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 9670b34..654f758 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -69,6 +69,7 @@
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.testing.EditInfoSubject;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -1158,7 +1159,10 @@
   @Test
   public void pushSameCommitTwice() throws Exception {
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    config
+        .getProject()
+        .setBooleanConfig(
+            BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
     saveProjectConfig(project, config);
 
     PushOneCommit push =
@@ -1182,7 +1186,10 @@
   @Test
   public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    config
+        .getProject()
+        .setBooleanConfig(
+            BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
     saveProjectConfig(project, config);
 
     PushOneCommit push =
@@ -1275,7 +1282,9 @@
     pushForReviewRejected(testRepo, "missing Change-Id in commit message footer");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
     saveProjectConfig(project, config);
     pushForReviewOk(testRepo);
   }
@@ -1301,7 +1310,9 @@
     pushForReviewRejected(testRepo, "multiple Change-Id lines in commit message footer");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
     saveProjectConfig(project, config);
     pushForReviewRejected(testRepo, "multiple Change-Id lines in commit message footer");
   }
@@ -1322,7 +1333,9 @@
     pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
     saveProjectConfig(project, config);
     pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
   }
@@ -1348,7 +1361,10 @@
     pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+    ;
     saveProjectConfig(project, config);
     pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
   }
@@ -1371,7 +1387,9 @@
             + " commit");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
     saveProjectConfig(project, config);
 
     pushForReviewRejected(
@@ -1394,7 +1412,9 @@
             + " commit");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setRequireChangeID(InheritableBoolean.FALSE);
+    config
+        .getProject()
+        .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
     saveProjectConfig(project, config);
 
     pushForReviewRejected(
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
index 0064570..cc4347b 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.server.git.ProjectConfig;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -82,7 +83,8 @@
 
   private void setRejectImplicitMerges() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    cfg.getProject().setRejectImplicitMerges(InheritableBoolean.TRUE);
+    cfg.getProject()
+        .setBooleanConfig(BooleanProjectConfig.REJECT_IMPLICIT_MERGES, InheritableBoolean.TRUE);
     saveProjectConfig(project, cfg);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index ccd5fca..3a41fae 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -56,6 +56,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -564,7 +565,10 @@
     // C0 -- Master
     //
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
-    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    config
+        .getProject()
+        .setBooleanConfig(
+            BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
     saveProjectConfig(project, config);
 
     PushOneCommit push1 =
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index f90a73b..9d811b2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -181,10 +182,14 @@
     assertProjectInfo(project, p);
     assertThat(project.getDescription()).isEqualTo(in.description);
     assertThat(project.getSubmitType()).isEqualTo(in.submitType);
-    assertThat(project.getUseContributorAgreements()).isEqualTo(in.useContributorAgreements);
-    assertThat(project.getUseSignedOffBy()).isEqualTo(in.useSignedOffBy);
-    assertThat(project.getUseContentMerge()).isEqualTo(in.useContentMerge);
-    assertThat(project.getRequireChangeID()).isEqualTo(in.requireChangeId);
+    assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS))
+        .isEqualTo(in.useContributorAgreements);
+    assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY))
+        .isEqualTo(in.useSignedOffBy);
+    assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE))
+        .isEqualTo(in.useContentMerge);
+    assertThat(project.getBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID))
+        .isEqualTo(in.requireChangeId);
   }
 
   @Test