Add REST API for migrating label functions to submit requirements

This change adds two REST API endpoints:

  POST /projects/foo/migrate-labels
  POST /projects/foo/migrate-labels:review

Both migrate label functions to submit-requirements. The first one
commits the migration result into the refs/meta/config branch.  The
second one prepares a migration change for review.

With these new endpoints, Gerrit admins can perform label function
migration stepwise and involve project owners into the review process.

Release-Notes: REST API for migrating label functions to submit requirements
Change-Id: Iacfa133f96f20ab885b925fa38a981fde5ae9e4b
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 1fe0eff..f85f284 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -4150,6 +4150,68 @@
   }
 ----
 
+[[migrate-labels]]
+=== Migrate label functions to submit requirements
+--
+'POST /projects/link:#project-name[\{project-name\}]/migrate-labels'
+--
+
+Migrates labels with functions to submit requirements. The migration result is
+committed into the `refs/meta/config` branch and thus immediately active. As a
+response it returns link:#migrate-labels-info[MigrateLabelsInfo] entity
+describing the outcome of the migration.
+
+The caller must be a project owner.
+
+.Request
+----
+  POST /projects/testproj/migrate-labels HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {"status": "MIGRATED"}
+----
+
+
+[[migrate-labels-change]]
+=== Create change which migrate label functions to submit requirements
+--
+'POST /projects/link:#project-name[\{project-name\}]/migrate-labels:review'
+--
+
+Creates a change for review which migrates labels with functions to submit requirements.
+As a response it returns link:#migrate-labels-review-info[MigrageLabelsReviewInfo] entity
+describing the outcome of the migration.
+
+.Request
+----
+  POST /projects/testproj/migrate-labels HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "status": "MIGRATED",
+    "change": {
+      "id": "testproj~12345",
+      ...
+    }
+  }
+----
+
+
+
 [[ids]]
 == IDs
 
@@ -5266,6 +5328,40 @@
 a date in the future.
 |=========================
 
+
+[[migrate-labels-info]]
+=== MigrateLabelsInfo
+The `MigrateLabelsInfo` entity contains information about an outcome of labels
+function migration.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`status`        ||The status of the migration. Takes one of the following values:
+`MIGRATED`,
+`HAS_PROLOG`,
+`PREVIOUSLY_MIGRATED`,
+`NO_CHANGE`
+|=============================
+
+[[migrate-labels-review-info]]
+=== MigrateLabelsReviewInfo
+The `MigrateLabelsReviewInfo` entity contains information about an outcome of creating
+a change for labels function migration.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`status`        ||The status of the migration. Takes one of the following values:
+`MIGRATED`,
+`HAS_PROLOG`,
+`PREVIOUSLY_MIGRATED`,
+`NO_CHANGE`
+|`change`        |optional|The change created.
+It is a link:rest-api-changes.html#change-info[ChangeInfo] entity
+and is set only when the `status` value is `MIGRATED`.
+|=============================
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 70ba6c5..47c6abe 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -27,6 +27,7 @@
         "//java/com/google/gerrit/server/flow",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//lib:args4j",
         "//lib:blame-cache",
diff --git a/java/com/google/gerrit/server/restapi/project/MigrateLabels.java b/java/com/google/gerrit/server/restapi/project/MigrateLabels.java
new file mode 100644
index 0000000..3e889a3
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/MigrateLabels.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2025 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.restapi.project;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
+import com.google.gerrit.server.schema.UpdateUI;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Set;
+
+@Singleton
+public class MigrateLabels implements RestModifyView<ProjectResource, MigrateLabelsInput> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final MigrateLabelFunctionsToSubmitRequirement migrateLabelFunctionsToSubmitRequirement;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  MigrateLabels(
+      MigrateLabelFunctionsToSubmitRequirement migrateLabelFunctionsToSubmitRequirement,
+      PermissionBackend permissionBackend) {
+    this.migrateLabelFunctionsToSubmitRequirement = migrateLabelFunctionsToSubmitRequirement;
+    this.permissionBackend = permissionBackend;
+  }
+
+  @Override
+  public Response<MigrateLabelsInfo> apply(ProjectResource rsrc, MigrateLabelsInput input)
+      throws Exception {
+    Project.NameKey project = rsrc.getNameKey();
+    permissionBackend.currentUser().project(project).check(ProjectPermission.WRITE_CONFIG);
+    MigrateLabelFunctionsToSubmitRequirement.Status status =
+        migrateLabelFunctionsToSubmitRequirement.executeMigration(project, new LoggingUpdateUI());
+
+    MigrateLabelsInfo info = new MigrateLabelsInfo();
+    info.status = status;
+    return Response.ok(info);
+  }
+
+  public static class LoggingUpdateUI implements UpdateUI {
+
+    @Override
+    public void message(String message) {
+      logger.atInfo().log(message);
+    }
+
+    @Override
+    public boolean yesno(boolean defaultValue, String message) {
+      return false;
+    }
+
+    @Override
+    public void waitForUser() {}
+
+    @Override
+    public String readString(String defaultValue, Set<String> allowedValues, String message) {
+      return null;
+    }
+
+    @Override
+    public boolean isBatch() {
+      return false;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/MigrateLabelsInfo.java b/java/com/google/gerrit/server/restapi/project/MigrateLabelsInfo.java
new file mode 100644
index 0000000..b6a2920
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/MigrateLabelsInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2025 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.restapi.project;
+
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
+
+public class MigrateLabelsInfo {
+  public MigrateLabelFunctionsToSubmitRequirement.Status status;
+}
diff --git a/java/com/google/gerrit/server/restapi/project/MigrateLabelsInput.java b/java/com/google/gerrit/server/restapi/project/MigrateLabelsInput.java
new file mode 100644
index 0000000..d010a9d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/MigrateLabelsInput.java
@@ -0,0 +1,17 @@
+// Copyright (C) 2025 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.restapi.project;
+
+public class MigrateLabelsInput {}
diff --git a/java/com/google/gerrit/server/restapi/project/MigrateLabelsReview.java b/java/com/google/gerrit/server/restapi/project/MigrateLabelsReview.java
new file mode 100644
index 0000000..0b31d63
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/MigrateLabelsReview.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2025 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.restapi.project;
+
+import static com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement.Status.MIGRATED;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class MigrateLabelsReview implements RestModifyView<ProjectResource, MigrateLabelsInput> {
+
+  private final RepoMetaDataUpdater repoMetaDataUpdater;
+  private final MigrateLabelFunctionsToSubmitRequirement migrateLabelFunctionsToSubmitRequirement;
+
+  @Inject
+  MigrateLabelsReview(
+      RepoMetaDataUpdater repoMetaDataUpdater,
+      MigrateLabelFunctionsToSubmitRequirement migrateLabelFunctionsToSubmitRequirement) {
+    this.repoMetaDataUpdater = repoMetaDataUpdater;
+    this.migrateLabelFunctionsToSubmitRequirement = migrateLabelFunctionsToSubmitRequirement;
+  }
+
+  @Override
+  public Response<MigrateLabelsReviewInfo> apply(ProjectResource rsrc, MigrateLabelsInput input)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    try (ConfigChangeCreator creator =
+        repoMetaDataUpdater.configChangeCreator(
+            rsrc.getNameKey(), null, MigrateLabelFunctionsToSubmitRequirement.COMMIT_MSG)) {
+      MigrateLabelFunctionsToSubmitRequirement.Status status =
+          migrateLabelFunctionsToSubmitRequirement.updateConfig(
+              rsrc.getProjectState().getNameKey(),
+              creator.getConfig(),
+              new MigrateLabels.LoggingUpdateUI());
+      if (status == MIGRATED) {
+        return Response.ok(new MigrateLabelsReviewInfo(MIGRATED, creator.createChange().value()));
+      }
+      return Response.ok(new MigrateLabelsReviewInfo(status));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/MigrateLabelsReviewInfo.java b/java/com/google/gerrit/server/restapi/project/MigrateLabelsReviewInfo.java
new file mode 100644
index 0000000..01d9640
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/MigrateLabelsReviewInfo.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2025 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.restapi.project;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
+
+public class MigrateLabelsReviewInfo {
+  public MigrateLabelFunctionsToSubmitRequirement.Status status;
+  public ChangeInfo change;
+
+  public MigrateLabelsReviewInfo(
+      MigrateLabelFunctionsToSubmitRequirement.Status status, ChangeInfo change) {
+    this.status = status;
+    this.change = change;
+  }
+
+  public MigrateLabelsReviewInfo(MigrateLabelFunctionsToSubmitRequirement.Status status) {
+    this(status, null);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index f5647ec..adba60e 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -87,6 +87,9 @@
     put(PROJECT_KIND, "config").to(PutConfig.class);
     put(PROJECT_KIND, "config:review").to(PutConfigReview.class);
 
+    post(PROJECT_KIND, "migrate-labels").to(MigrateLabels.class);
+    post(PROJECT_KIND, "migrate-labels:review").to(MigrateLabelsReview.class);
+
     post(PROJECT_KIND, "create.change").to(CreateChange.class);
 
     child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index 37d9802..bdc521e 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -135,7 +135,7 @@
     }
   }
 
-  private Status updateConfig(Project.NameKey project, ProjectConfig projectConfig, UpdateUI ui)
+  public Status updateConfig(Project.NameKey project, ProjectConfig projectConfig, UpdateUI ui)
       throws IOException {
     boolean updated = false;
     if (hasPrologRules(project)) {