Revert "Remove requireChangeId."

This reverts

* commit ec79eb968296c5900b483c87439901a3ce8862df (I2c407bdded0,
  "Remove requireChangeId.")

* commit 80beb1cac22c5e1607c6235dc6f85ecd86d21327 (I409106ce "Remove
  fields/methods that are unused since requireChangeId has been
  removed")

Reason for revert: This uncovered internal users at Google.

In particular, PutMessage, which is a commonly used REST API endpoint,
requires a Change-Id to be included if requireChangeId=false.

Change-Id: I626dbb4d53fbcbf5983b0a5e79ceadbee8a42db9
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 08c26d4..71af331 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -157,6 +157,32 @@
 did  what, especially with patches. Default is `INHERIT`, which means that this
 property is inherited from the parent project.
 
+[[receive.requireChangeId]]receive.requireChangeId::
++
+The `Require Change-Id in commit message` option defines whether a
+link:user-changeid.html[Change-Id] in the commit message is required
+for pushing a commit for review. If this option is set, trying to push
+a commit for review that doesn't contain a Change-Id in the commit
+message fails with link:error-missing-changeid.html[missing Change-Id
+in commit message footer].
+
+It is recommended to set this option and use a
+link:user-changeid.html#create[commit-msg hook] (or other client side
+tooling like EGit) to automatically generate Change-Id's for new
+commits. This way the Change-Id is automatically in place when changes
+are reworked or rebased and uploading new patch sets gets easy.
+
+If this option is not set, commits can be uploaded without a Change-Id,
+but then users have to remember to copy the assigned Change-Id from the
+change screen and insert it manually into the commit message when they
+want to upload a second patch set.
+
+Default is `INHERIT`, which means that this property is inherited from
+the parent project. The global default for new hosts is `true`
+
+This option is deprecated and future releases will behave as if this
+is always `true`.
+
 [[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
 +
 Maximum allowed Git object size that receive-pack will accept. If an object
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 1544aae..f69c4ae 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -824,6 +824,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "require_change_id": {
+      "value": false,
+      "configured_value": "FALSE",
+      "inherited_value": true
+    },
     "max_object_size_limit": {
       "value": "15m",
       "configured_value": "15m",
@@ -882,6 +887,7 @@
     "enable_signed_push": "INHERIT",
     "require_signed_push": "INHERIT",
     "reject_implicit_merges": "INHERIT",
+    "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
     "state": "ACTIVE"
@@ -919,6 +925,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "require_change_id": {
+      "value": true,
+      "configured_value": "TRUE",
+      "inherited_value": true
+    },
     "enable_signed_push": {
       "value": true,
       "configured_value": "INHERIT",
@@ -3083,6 +3094,12 @@
 |`create_new_change_for_all_not_in_target` |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 a new change is created for every commit not in target branch.
+|`require_change_id`                       |optional|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
+valid link:user-changeid.html[Change-Id] footer in any commit uploaded
+for review is required. This does not apply to commits pushed directly
+to a branch or tag. This property is deprecated and will be removed in
+a future release.
 |`enable_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
@@ -3163,6 +3180,14 @@
 branch. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
+|`require_change_id`                       |optional|
+Whether a valid link:user-changeid.html[Change-Id] footer in any commit
+uploaded for review is required. This does not apply to commits pushed
+directly to a branch or tag. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+This property is deprecated and will be removed in
+a future release.
 |`reject_implicit_merges`                  |optional|
 Whether a check for implicit merges will be performed when changes
 are pushed for review. +
@@ -3528,6 +3553,11 @@
 Whether content merge should be enabled for the project (`TRUE`,
 `FALSE`, `INHERIT`). +
 `FALSE`, if the `submit_type` is `FAST_FORWARD_ONLY`.
+|`require_change_id`                           |`INHERIT` if not set|
+Whether the usage of Change-Ids is required for the project (`TRUE`,
+`FALSE`, `INHERIT`).
+This property is deprecated and will be removed in
+a future release.
 |`enable_signed_push`                           |`INHERIT` if not set|
 Whether signed push validation is enabled on the project  (`TRUE`,
 `FALSE`, `INHERIT`).
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8818ade..197a6a3 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -888,6 +888,15 @@
     }
   }
 
+  protected void setRequireChangeId(InheritableBoolean value) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = projectConfigFactory.read(md);
+      config.getProject().setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
diff --git a/java/com/google/gerrit/acceptance/TestProjectInput.java b/java/com/google/gerrit/acceptance/TestProjectInput.java
index 7deb88a..0a3686b 100644
--- a/java/com/google/gerrit/acceptance/TestProjectInput.java
+++ b/java/com/google/gerrit/acceptance/TestProjectInput.java
@@ -43,6 +43,8 @@
 
   InheritableBoolean useContentMerge() default InheritableBoolean.INHERIT;
 
+  InheritableBoolean requireChangeId() default InheritableBoolean.INHERIT;
+
   InheritableBoolean rejectEmptyCommit() default InheritableBoolean.INHERIT;
 
   InheritableBoolean enableSignedPush() default InheritableBoolean.INHERIT;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index eddfb09..fb2a0fe 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -29,6 +29,7 @@
   public InheritedBooleanInfo useContentMerge;
   public InheritedBooleanInfo useSignedOffBy;
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
+  public InheritedBooleanInfo requireChangeId;
   public InheritedBooleanInfo enableSignedPush;
   public InheritedBooleanInfo requireSignedPush;
   public InheritedBooleanInfo rejectImplicitMerges;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 44a5258..1a6d77b 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -25,6 +25,7 @@
   public InheritableBoolean useContentMerge;
   public InheritableBoolean useSignedOffBy;
   public InheritableBoolean createNewChangeForAllNotInTarget;
+  public InheritableBoolean requireChangeId;
   public InheritableBoolean enableSignedPush;
   public InheritableBoolean requireSignedPush;
   public InheritableBoolean rejectImplicitMerges;
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index 2dec2b9..e61d316 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -31,6 +31,7 @@
   public InheritableBoolean useContributorAgreements;
   public InheritableBoolean useSignedOffBy;
   public InheritableBoolean useContentMerge;
+  public InheritableBoolean requireChangeId;
   public InheritableBoolean createNewChangeForAllNotInTarget;
   public InheritableBoolean rejectEmptyCommit;
   public InheritableBoolean enableSignedPush;
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
index bea5abe..a70d254 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -32,6 +32,7 @@
   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"),
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 02a24f7..a9a1a5d 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -146,7 +146,12 @@
               new CommitterUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
-                  user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
+                  projectState,
+                  user,
+                  urlFormatter.get(),
+                  installCommitMsgHookCommand,
+                  sshInfo,
+                  change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators, skipValidation),
@@ -173,7 +178,12 @@
               new AuthorUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())),
               new ChangeIdValidator(
-                  user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
+                  projectState,
+                  user,
+                  urlFormatter.get(),
+                  installCommitMsgHookCommand,
+                  sshInfo,
+                  change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -247,6 +257,7 @@
 
     private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
 
+    private final ProjectState projectState;
     private final UrlFormatter urlFormatter;
     private final String installCommitMsgHookCommand;
     private final SshInfo sshInfo;
@@ -254,11 +265,13 @@
     private final Change change;
 
     public ChangeIdValidator(
+        ProjectState projectState,
         IdentifiedUser user,
         UrlFormatter urlFormatter,
         String installCommitMsgHookCommand,
         SshInfo sshInfo,
         Change change) {
+      this.projectState = projectState;
       this.urlFormatter = urlFormatter;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
@@ -294,8 +307,10 @@
                   Type.ERROR));
           throw new CommitValidationException(CHANGE_ID_ABOVE_FOOTER_MSG, messages);
         }
-        messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
-        throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
+        if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
+          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
+          throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
+        }
       } else if (idList.size() > 1) {
         throw new CommitValidationException(MULTIPLE_CHANGE_ID_MSG, messages);
       } else {
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 1328f6b..79eccbb 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -39,6 +39,9 @@
               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,
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 9ae3b2c..7405df1 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -33,6 +33,7 @@
   public List<String> branch;
   public InheritableBoolean contentMerge;
   public InheritableBoolean newChangeForAllNotInTarget;
+  public InheritableBoolean changeIdRequired;
   public InheritableBoolean rejectEmptyCommit;
   public InheritableBoolean enableSignedPush;
   public InheritableBoolean requireSignedPush;
@@ -43,6 +44,7 @@
     contributorAgreements = InheritableBoolean.INHERIT;
     signedOffBy = InheritableBoolean.INHERIT;
     contentMerge = InheritableBoolean.INHERIT;
+    changeIdRequired = InheritableBoolean.INHERIT;
     newChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     enableSignedPush = InheritableBoolean.INHERIT;
     requireSignedPush = InheritableBoolean.INHERIT;
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index d468b44..b50b046 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -152,6 +152,7 @@
       newProject.setBooleanConfig(
           BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
           args.newChangeForAllNotInTarget);
+      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
       newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
       newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
       newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush);
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 5c36e60..db02418 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -22,6 +22,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.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -110,7 +111,10 @@
     String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
 
     ensureCanEditCommitMessage(resource.getNotes());
-    ensureChangeIdIsCorrect(resource.getChange().getKey().get(), sanitizedCommitMessage);
+    ensureChangeIdIsCorrect(
+        projectCache.checkedGet(resource.getProject()).is(BooleanProjectConfig.REQUIRE_CHANGE_ID),
+        resource.getChange().getKey().get(),
+        sanitizedCommitMessage);
 
     try (Repository repository = repositoryManager.openRepository(resource.getProject());
         RevWalk revWalk = new RevWalk(repository);
@@ -189,7 +193,8 @@
     }
   }
 
-  private static void ensureChangeIdIsCorrect(String currentChangeId, String newCommitMessage)
+  private static void ensureChangeIdIsCorrect(
+      boolean requireChangeId, String currentChangeId, String newCommitMessage)
       throws ResourceConflictException, BadRequestException {
     RevCommit revCommit =
         RevCommit.parse(
@@ -199,7 +204,7 @@
     CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
 
     List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID);
-    if (changeIdFooters.isEmpty()) {
+    if (requireChangeId && changeIdFooters.isEmpty()) {
       throw new ResourceConflictException("missing Change-Id footer");
     }
     if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 6428435..6844cac 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -147,6 +147,8 @@
     args.newChangeForAllNotInTarget =
         MoreObjects.firstNonNull(
             input.createNewChangeForAllNotInTarget, InheritableBoolean.INHERIT);
+    args.changeIdRequired =
+        MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
     args.rejectEmptyCommit =
         MoreObjects.firstNonNull(input.rejectEmptyCommit, InheritableBoolean.INHERIT);
     args.enableSignedPush =
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
index f72bf4d..7231b18 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsInput.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.java
@@ -33,6 +33,8 @@
   public static final ImmutableMap<BooleanProjectConfig, InheritableBoolean>
       DEFAULT_BOOLEAN_PROJECT_CONFIGS =
           ImmutableMap.of(
+              BooleanProjectConfig.REQUIRE_CHANGE_ID,
+              InheritableBoolean.TRUE,
               BooleanProjectConfig.USE_CONTENT_MERGE,
               InheritableBoolean.TRUE,
               BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS,
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
index 6c49b22..be56782 100644
--- a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -40,6 +40,7 @@
           "[receive]",
           "  requireContributorAgreement = false",
           "  requireSignedOffBy = false",
+          "  requireChangeId = true",
           "  enableSignedPush = false");
   private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_SUBMIT_SECTION =
       ImmutableList.of("[submit]", "  mergeContent = true");
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 56e72e6..df86d63 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -91,6 +91,9 @@
   @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
   private InheritableBoolean contentMerge = InheritableBoolean.INHERIT;
 
+  @Option(name = "--change-id", usage = "if change-id is required")
+  private InheritableBoolean requireChangeID = InheritableBoolean.INHERIT;
+
   @Option(name = "--reject-empty-commit", usage = "if empty commits should be rejected on submit")
   private InheritableBoolean rejectEmptyCommit = InheritableBoolean.INHERIT;
 
@@ -121,6 +124,14 @@
   }
 
   @Option(
+      name = "--require-change-id",
+      aliases = {"--id"},
+      usage = "if change-id is required")
+  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
+    requireChangeID = InheritableBoolean.TRUE;
+  }
+
+  @Option(
       name = "--create-new-change-for-all-not-in-target",
       aliases = {"--ncfa"},
       usage = "if a new change will be created for every commit not in target branch")
@@ -175,6 +186,7 @@
         input.useContributorAgreements = contributorAgreements;
         input.useSignedOffBy = signedOffBy;
         input.useContentMerge = contentMerge;
+        input.requireChangeId = requireChangeID;
         input.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
         input.branches = branch;
         input.createEmptyCommit = createEmptyCommit;
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index f145b9e..8c9fc9f 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -56,6 +56,9 @@
   @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
   private InheritableBoolean contentMerge;
 
+  @Option(name = "--change-id", usage = "if change-id is required")
+  private InheritableBoolean requireChangeID;
+
   @Option(
       name = "--use-contributor-agreements",
       aliases = {"--ca"},
@@ -101,6 +104,22 @@
   }
 
   @Option(
+      name = "--require-change-id",
+      aliases = {"--id"},
+      usage = "if change-id is required")
+  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
+    requireChangeID = InheritableBoolean.TRUE;
+  }
+
+  @Option(
+      name = "--no-change-id",
+      aliases = {"--nid"},
+      usage = "if change-id is not required")
+  void setNoChangeId(@SuppressWarnings("unused") boolean on) {
+    requireChangeID = InheritableBoolean.FALSE;
+  }
+
+  @Option(
       name = "--project-state",
       aliases = {"--ps"},
       usage = "project's visibility state")
@@ -114,6 +133,7 @@
   @Override
   protected void run() throws Failure {
     ConfigInput configInput = new ConfigInput();
+    configInput.requireChangeId = requireChangeID;
     configInput.submitType = submitType;
     configInput.useContentMerge = contentMerge;
     configInput.useContributorAgreements = contributorAgreements;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 6d1c223..5c2f033 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3843,6 +3843,24 @@
   }
 
   @Test
+  public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
+    ConfigInput configInput = new ConfigInput();
+    configInput.requireChangeId = InheritableBoolean.FALSE;
+    gApi.projects().name(project.get()).config(configInput);
+
+    PushOneCommit.Result r = createChange();
+    r.assertOkStatus();
+    assertThat(getCommitMessage(r.getChangeId()))
+        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+
+    String newMessage = "modified commit\n";
+    gApi.changes().id(r.getChangeId()).setMessage(newMessage);
+    RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
+    assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
+    assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+  }
+
+  @Test
   public void changeCommitMessageWithNoChangeIdFails() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(getCommitMessage(r.getChangeId()))
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 05295ae..27dd16a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -65,7 +66,7 @@
 
   @Test
   public void detectAutoCloseableChangeByCommit() throws Exception {
-    RevCommit commit = pushCommitForReview();
+    RevCommit commit = pushCommitWithoutChangeIdForReview();
     ChangeInfo change =
         Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
 
@@ -89,7 +90,7 @@
 
   @Test
   public void fixAutoCloseableChangeByCommit() throws Exception {
-    RevCommit commit = pushCommitForReview();
+    RevCommit commit = pushCommitWithoutChangeIdForReview();
     ChangeInfo change =
         Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
 
@@ -279,7 +280,8 @@
                 + ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT);
   }
 
-  private RevCommit pushCommitForReview() throws Exception {
+  private RevCommit pushCommitWithoutChangeIdForReview() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
     RevCommit commit =
         testRepo
             .branch("HEAD")
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index da0cd43..a9c7d99 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -316,6 +316,7 @@
     assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
         .isEqualTo(input.createNewChangeForAllNotInTarget);
+    assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId);
     assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges);
     assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
@@ -345,6 +346,7 @@
     assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
         .isEqualTo(input.createNewChangeForAllNotInTarget);
+    assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId);
     assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges);
     assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
@@ -681,6 +683,7 @@
     input.useContentMerge = InheritableBoolean.TRUE;
     input.useSignedOffBy = InheritableBoolean.TRUE;
     input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
+    input.requireChangeId = InheritableBoolean.TRUE;
     input.rejectImplicitMerges = InheritableBoolean.TRUE;
     input.enableReviewerByEmail = InheritableBoolean.TRUE;
     input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 3d9b88d..c85aa93 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -89,6 +89,7 @@
 import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.mail.Address;
 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;
@@ -123,6 +124,7 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
@@ -457,6 +459,20 @@
   }
 
   @Test
+  public void pushWithoutChangeIdDeprecated() throws Exception {
+    setRequireChangeId(InheritableBoolean.FALSE);
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message("A change")
+        .author(admin.newIdent())
+        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
+        .create();
+    PushResult result = pushHead(testRepo, "refs/for/master");
+    assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated");
+  }
+
+  @Test
   public void autocloseByChangeId() throws Exception {
     // Create a change
     PushOneCommit.Result r = pushTo("refs/for/master");
@@ -1556,6 +1572,9 @@
     RevCommit c = createCommit(testRepo, "Message without Change-Id");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
     pushForReviewRejected(testRepo, "missing Change-Id in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewOk(testRepo);
   }
 
   @Test
@@ -1579,6 +1598,9 @@
                 + "More text, uh oh.\n");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
     pushForReviewRejected(testRepo, "Change-Id must be in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
   }
 
   @Test
@@ -1615,6 +1637,9 @@
             + "Change-Id: I10f98c2ef76e52e23aa23be5afeb71e40b350e86\n"
             + "Change-Id: Ie9a132e107def33bdd513b7854b50de911edba0a\n");
     pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer");
   }
 
   @Test
@@ -1631,6 +1656,9 @@
   private void testpushWithInvalidChangeId() throws Exception {
     createCommit(testRepo, "Message with invalid Change-Id\n\nChange-Id: X\n");
     pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
   }
 
   @Test
@@ -1652,12 +1680,18 @@
             + "\n"
             + "Change-Id: I0000000000000000000000000000000000000000\n");
     pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
   }
 
   @Test
   public void pushWithChangeIdInSubjectLine() throws Exception {
     createCommit(testRepo, "Change-Id: I1234000000000000000000000000000000000000");
     pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer");
+
+    setRequireChangeId(InheritableBoolean.FALSE);
+    pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer");
   }
 
   @Test
@@ -1675,6 +1709,19 @@
         "same Change-Id in multiple changes.\n"
             + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
             + " commit");
+
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .getProject()
+          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+      u.save();
+    }
+
+    pushForReviewRejected(
+        testRepo,
+        "same Change-Id in multiple changes.\n"
+            + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+            + " commit");
   }
 
   @Test
@@ -1688,6 +1735,19 @@
         "same Change-Id in multiple changes.\n"
             + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
             + " commit");
+
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .getProject()
+          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+      u.save();
+    }
+
+    pushForReviewRejected(
+        testRepo,
+        "same Change-Id in multiple changes.\n"
+            + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+            + " commit");
   }
 
   private static RevCommit createCommit(TestRepository<?> testRepo, String message)
@@ -2663,6 +2723,10 @@
     return cds.get(0);
   }
 
+  private static void pushForReviewOk(TestRepository<?> testRepo) throws GitAPIException {
+    pushForReview(testRepo, RemoteRefUpdate.Status.OK, null);
+  }
+
   private static void pushForReviewRejected(TestRepository<?> testRepo, String expectedMessage)
       throws GitAPIException {
     pushForReview(testRepo, RemoteRefUpdate.Status.REJECTED_OTHER_REASON, expectedMessage);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index b102619..043bde7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -218,6 +218,7 @@
     in.useContributorAgreements = InheritableBoolean.TRUE;
     in.useSignedOffBy = InheritableBoolean.TRUE;
     in.useContentMerge = InheritableBoolean.TRUE;
+    in.requireChangeId = InheritableBoolean.TRUE;
     ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(Project.nameKey(newProjectName)).getProject();
@@ -230,6 +231,8 @@
         .isEqualTo(in.useSignedOffBy);
     assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE))
         .isEqualTo(in.useContentMerge);
+    assertThat(project.getBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID))
+        .isEqualTo(in.requireChangeId);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index aacd41b..75e1cd7 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS;
+import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.REQUIRE_CHANGE_ID;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -612,13 +612,13 @@
   public void readAllProjectsBaseConfigFromSitePaths() throws Exception {
     ProjectConfig cfg = factory.create(ALL_PROJECTS);
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
         .isEqualTo(InheritableBoolean.INHERIT);
 
-    writeDefaultAllProjectsConfig("[receive]", "requireContributorAgreement = false");
+    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
 
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
         .isEqualTo(InheritableBoolean.FALSE);
   }
 
@@ -626,17 +626,17 @@
   public void readOtherProjectIgnoresAllProjectsBaseConfig() throws Exception {
     ProjectConfig cfg = factory.create(Project.nameKey("test"));
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
         .isEqualTo(InheritableBoolean.INHERIT);
 
-    writeDefaultAllProjectsConfig("[receive]", "requireContributorAgreement = false");
+    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(USE_CONTRIBUTOR_AGREEMENTS))
+    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
         .isEqualTo(InheritableBoolean.INHERIT);
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 044e1fe..52f143c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -154,6 +154,21 @@
                   </gr-select>
                 </span>
               </section>
+              <section>
+                <span class="title">Require Change-Id in commit message</span>
+                <span class="value">
+                  <gr-select
+                      id="requireChangeIdSelect"
+                      bind-value="{{_repoConfig.require_change_id.configured_value}}">
+                    <select disabled$="[[_readOnly]]">
+                      <template is="dom-repeat"
+                          items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
+                        <option value="[[item.value]]">[[item.label]]</option>
+                      </template>
+                    </select>
+                  </gr-select>
+                </span>
+              </section>
               <section
                    id="enableSignedPushSettings"
                    class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index 744ef42..53db6d5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -57,6 +57,10 @@
         value: false,
         configured_value: 'FALSE',
       },
+      require_change_id: {
+        value: false,
+        configured_value: 'FALSE',
+      },
       enable_signed_push: {
         value: false,
         configured_value: 'FALSE',
@@ -318,6 +322,7 @@
           use_content_merge: 'TRUE',
           use_signed_off_by: 'TRUE',
           create_new_change_for_all_not_in_target: 'TRUE',
+          require_change_id: 'TRUE',
           enable_signed_push: 'TRUE',
           require_signed_push: 'TRUE',
           reject_implicit_merges: 'TRUE',
@@ -347,6 +352,8 @@
               configInputObj.use_content_merge;
           element.$.newChangeSelect.bindValue =
               configInputObj.create_new_change_for_all_not_in_target;
+          element.$.requireChangeIdSelect.bindValue =
+              configInputObj.require_change_id;
           element.$.enableSignedPush.bindValue =
               configInputObj.enable_signed_push;
           element.$.requireSignedPush.bindValue =