Merge "doc: Document necessity of using 'http.saveCookies'"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a12e1ce..e2e3f71 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -8629,7 +8629,9 @@
=== IncludedInInfo
The `IncludedInInfo` entity contains information about the branches a
change was merged into and tags it was tagged with. The branch or tag
-must have 'refs/head/' or 'refs/tags/' prefix respectively.
+must have 'refs/heads/' or 'refs/tags/' prefix respectively.
+
+Branch and tag lists are sorted in lexicographical order.
[options="header",cols="1,^1,5"]
|=======================
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index e00749d..16dab80 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/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a6e659b..d6df3d8 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -139,6 +139,7 @@
import com.google.gerrit.server.plugins.TestServerPlugin;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.Revisions;
@@ -268,6 +269,7 @@
@Inject protected FakeEmailSender sender;
@Inject protected GerritApi gApi;
@Inject protected GitRepositoryManager repoManager;
+ @Inject protected RepoMetaDataUpdater repoMetaDataUpdater;
@Inject protected GroupBackend groupBackend;
@Inject protected GroupCache groupCache;
@Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/project/RepoMetaDataUpdater.java
similarity index 98%
rename from java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
rename to java/com/google/gerrit/server/project/RepoMetaDataUpdater.java
index a34b7d8..e617f34 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/project/RepoMetaDataUpdater.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.restapi.project;
+package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -42,8 +42,6 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
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/AbstractPostCollection.java b/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
index 6a99bed..1680b21 100644
--- a/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Provider;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 5fc060a..96090ed 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -28,7 +28,8 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index 35102d8..3809929 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -36,6 +36,7 @@
import com.google.gerrit.server.project.LabelResource;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
index 86d42cb..057f493 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
import com.google.gerrit.server.project.SubmitRequirementJson;
import com.google.gerrit.server.project.SubmitRequirementResource;
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
index b38aa29..2482a28 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.LabelResource;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
index 64e2399..ebc5461 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.project.SubmitRequirementResource;
import com.google.inject.Inject;
import com.google.inject.Singleton;
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/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java
index 2182784..1900e28 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabels.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -24,6 +24,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.LabelResource;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
index 4e9d432..d0562c6 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
@@ -24,7 +24,8 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import java.io.IOException;
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
index 71080a5..3848d75 100644
--- a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
@@ -22,6 +22,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.project.SubmitRequirementResource;
import com.google.inject.Provider;
import javax.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
index f0a371a..4e686fe 100644
--- a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
@@ -22,7 +22,8 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import java.io.IOException;
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/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index afd07bb..39ded77 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -55,7 +55,8 @@
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfigReview.java b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
index 7e6cc19..3e34227 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
@@ -21,7 +21,8 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import java.io.IOException;
import javax.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/project/PutDescription.java b/java/com/google/gerrit/server/restapi/project/PutDescription.java
index ea685c8..0841d6b 100644
--- a/java/com/google/gerrit/server/restapi/project/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/PutDescription.java
@@ -26,6 +26,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 65851c0..286cf90 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -36,7 +36,8 @@
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index a46ee32..d5b4a94 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.project.DashboardResource;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index 33d4640..b894b44 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.project.LabelDefinitionJson;
import com.google.gerrit.server.project.LabelResource;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index b94e911..074e5f8 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -47,6 +47,7 @@
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
diff --git a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
index d3a251e..3d31e2b3 100644
--- a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
@@ -27,6 +27,7 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
import com.google.gerrit.server.project.SubmitRequirementJson;
import com.google.gerrit.server.project.SubmitRequirementResource;
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index c6d7dc9..bdc521e 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -16,35 +16,30 @@
import static com.google.gerrit.server.project.ProjectConfig.RULES_PL_FILE;
-import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.git.meta.VersionedConfigFile;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.RepoMetaDataUpdater;
+import com.google.gerrit.server.project.RepoMetaDataUpdater.ConfigUpdater;
+import com.google.inject.Inject;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
-import javax.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -83,8 +78,9 @@
*/
public class MigrateLabelFunctionsToSubmitRequirement {
public static final String COMMIT_MSG = "Migrate label functions to submit requirements";
+
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
private final GitRepositoryManager repoManager;
- private final PersonIdent serverUser;
public enum Status {
/**
@@ -111,9 +107,9 @@
@Inject
public MigrateLabelFunctionsToSubmitRequirement(
- GitRepositoryManager repoManager, @GerritPersonIdent PersonIdent serverUser) {
+ RepoMetaDataUpdater repoMetaDataUpdater, GitRepositoryManager repoManager) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
this.repoManager = repoManager;
- this.serverUser = serverUser;
}
/**
@@ -125,178 +121,99 @@
* @return {@link Status} reflecting the status of the migration.
*/
public Status executeMigration(Project.NameKey project, UpdateUI ui)
- throws IOException, ConfigInvalidException {
+ throws IOException,
+ ConfigInvalidException,
+ MethodNotAllowedException,
+ PermissionBackendException {
+ try (ConfigUpdater updater =
+ repoMetaDataUpdater.configUpdaterWithoutPermissionsCheck(project, null, COMMIT_MSG)) {
+ Status result = updateConfig(project, updater.getConfig(), ui);
+ if (result == Status.MIGRATED) {
+ updater.commitConfigUpdate();
+ }
+ return result;
+ }
+ }
+
+ public Status updateConfig(Project.NameKey project, ProjectConfig projectConfig, UpdateUI ui)
+ throws IOException {
+ boolean updated = false;
if (hasPrologRules(project)) {
ui.message(String.format("Skipping project %s because it has prolog rules", project));
return Status.HAS_PROLOG;
}
- VersionedConfigFile projectConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
- boolean migrationPerformed = false;
- try (Repository repo = repoManager.openRepository(project);
- MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) {
- if (hasMigrationAlreadyRun(repo)) {
- ui.message(
- String.format(
- "Skipping migrating label functions to submit requirements for project '%s'"
- + " because it has been previously migrated",
- project));
- return Status.PREVIOUSLY_MIGRATED;
+
+ if (hasMigrationAlreadyRun(project)) {
+ ui.message(
+ String.format(
+ "Skipping migrating label functions to submit requirements for project '%s'"
+ + " because it has been previously migrated",
+ project));
+ return Status.PREVIOUSLY_MIGRATED;
+ }
+
+ Map<String, LabelType> labelSections = projectConfig.getLabelSections();
+ SubmitRequirementMap existingSubmitRequirements =
+ new SubmitRequirementMap(projectConfig.getSubmitRequirementSections());
+
+ for (Map.Entry<String, LabelType> section : labelSections.entrySet()) {
+ String labelName = section.getKey();
+ LabelType labelType = section.getValue();
+
+ if (labelType.getFunction() == LabelFunction.PATCH_SET_LOCK) {
+ // PATCH_SET_LOCK functions should be left as is
+ continue;
}
- projectConfig.load(project, repo);
- Config cfg = projectConfig.getConfig();
- Map<String, LabelAttributes> labelTypes = getLabelTypes(cfg);
- Map<String, SubmitRequirement> existingSubmitRequirements = loadSubmitRequirements(cfg);
- boolean updated = false;
- for (Map.Entry<String, LabelAttributes> lt : labelTypes.entrySet()) {
- String labelName = lt.getKey();
- LabelAttributes attributes = lt.getValue();
- if (attributes.function().equals("PatchSetLock")) {
- // PATCH_SET_LOCK functions should be left as is
- continue;
- }
- // If the function is other than "NoBlock" we want to reset the label function regardless
- // of whether there exists a "submit requirement".
- if (!attributes.function().equals("NoBlock")) {
- updated = true;
- writeLabelFunction(cfg, labelName, "NoBlock");
- }
- Optional<SubmitRequirement> sr = createSrFromLabelDef(labelName, attributes);
- if (!sr.isPresent()) {
- continue;
- }
- // Make the operation idempotent by skipping creating the submit-requirement if one was
- // already created or previously existed.
- if (existingSubmitRequirements.containsKey(labelName.toLowerCase(Locale.ROOT))) {
- if (!sr.get()
- .equals(existingSubmitRequirements.get(labelName.toLowerCase(Locale.ROOT)))) {
- ui.message(
- String.format(
- "Warning: Skipping creating a submit requirement for label '%s'. An existing "
- + "submit requirement is already present but its definition is not "
- + "identical to the existing label definition.",
- labelName));
- }
- continue;
- }
+
+ // If the function is other than "NoBlock" we want to reset the label function regardless
+ // of whether there exists a "submit requirement".
+ if (labelType.getFunction() != LabelFunction.NO_BLOCK) {
+ section.setValue(labelType.toBuilder().setNoBlockFunction().build());
updated = true;
- ui.message(
- String.format(
- "Project %s: Creating a submit requirement for label %s", project, labelName));
- writeSubmitRequirement(cfg, sr.get());
}
- if (updated) {
- commit(projectConfig, md);
- migrationPerformed = true;
+
+ Optional<SubmitRequirement> sr = createSrFromLabelDef(labelType);
+ if (!sr.isPresent()) {
+ continue;
}
+ // Make the operation idempotent by skipping creating the submit-requirement if one was
+ // already created or previously existed.
+ if (existingSubmitRequirements.containsKey(labelName)) {
+ SubmitRequirement existing = existingSubmitRequirements.get(labelName);
+ if (!sr.get().equals(existing)) {
+ ui.message(
+ String.format(
+ "Warning: Skipping creating a submit requirement for label '%s'. An existing "
+ + "submit requirement is already present but its definition is not "
+ + "identical to the existing label definition.",
+ labelName));
+ }
+ continue;
+ }
+ updated = true;
+ ui.message(
+ String.format(
+ "Project %s: Creating a submit requirement for label %s", project, labelName));
+ existingSubmitRequirements.put(sr.get());
}
- return migrationPerformed ? Status.MIGRATED : Status.NO_CHANGE;
+ return updated ? Status.MIGRATED : Status.NO_CHANGE;
}
- /**
- * Returns a Map containing label names as string in keys along with some of its attributes (that
- * we need in the migration) like canOverride, ignoreSelfApproval and function in the value.
- */
- private Map<String, LabelAttributes> getLabelTypes(Config cfg) {
- Map<String, LabelAttributes> labelTypes = new HashMap<>();
- for (String labelName : cfg.getSubsections(ProjectConfig.LABEL)) {
- String function = cfg.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
- boolean canOverride =
- cfg.getBoolean(
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_CAN_OVERRIDE,
- /* defaultValue= */ true);
- boolean ignoreSelfApproval =
- cfg.getBoolean(
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_IGNORE_SELF_APPROVAL,
- /* defaultValue= */ false);
- ImmutableList<String> values =
- ImmutableList.<String>builder()
- .addAll(
- Arrays.asList(
- cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_VALUE)))
- .build();
- ImmutableList<String> refPatterns =
- ImmutableList.<String>builder()
- .addAll(
- Arrays.asList(
- cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_BRANCH)))
- .build();
- LabelAttributes attributes =
- LabelAttributes.create(
- function == null ? "MaxWithBlock" : function,
- canOverride,
- ignoreSelfApproval,
- values,
- refPatterns);
- labelTypes.put(labelName, attributes);
- }
- return labelTypes;
- }
-
- private void writeSubmitRequirement(Config cfg, SubmitRequirement sr) {
- if (sr.description().isPresent()) {
- cfg.setString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- sr.name(),
- ProjectConfig.KEY_SR_DESCRIPTION,
- sr.description().get());
- }
- if (sr.applicabilityExpression().isPresent()) {
- cfg.setString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- sr.name(),
- ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
- sr.applicabilityExpression().get().expressionString());
- }
- cfg.setString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- sr.name(),
- ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- sr.submittabilityExpression().expressionString());
- if (sr.overrideExpression().isPresent()) {
- cfg.setString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- sr.name(),
- ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
- sr.overrideExpression().get().expressionString());
- }
- cfg.setBoolean(
- ProjectConfig.SUBMIT_REQUIREMENT,
- sr.name(),
- ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
- sr.allowOverrideInChildProjects());
- }
-
- private void writeLabelFunction(Config cfg, String labelName, String function) {
- cfg.setString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, function);
- }
-
- private void commit(VersionedConfigFile projectConfig, MetaDataUpdate md) throws IOException {
- md.getCommitBuilder().setAuthor(serverUser);
- md.getCommitBuilder().setCommitter(serverUser);
- md.setMessage(COMMIT_MSG);
- projectConfig.commit(md);
- }
-
- private static Optional<SubmitRequirement> createSrFromLabelDef(
- String labelName, LabelAttributes attributes) {
- if (isLabelSkipped(attributes.values())) {
- return Optional.of(createNonApplicableSr(labelName, attributes.canOverride()));
- } else if (isBlockingOrRequiredLabel(attributes.function())) {
- return Optional.of(createBlockingOrRequiredSr(labelName, attributes));
+ private static Optional<SubmitRequirement> createSrFromLabelDef(LabelType lt) {
+ if (isLabelSkipped(lt)) {
+ return Optional.of(createNonApplicableSr(lt));
+ } else if (isBlockingOrRequiredLabel(lt)) {
+ return Optional.of(createBlockingOrRequiredSr(lt));
}
return Optional.empty();
}
- private static SubmitRequirement createNonApplicableSr(String labelName, boolean canOverride) {
+ private static SubmitRequirement createNonApplicableSr(LabelType lt) {
return SubmitRequirement.builder()
- .setName(labelName)
+ .setName(lt.getName())
.setApplicabilityExpression(SubmitRequirementExpression.of("is:false"))
.setSubmittabilityExpression(SubmitRequirementExpression.create("is:true"))
- .setAllowOverrideInChildProjects(canOverride)
+ .setAllowOverrideInChildProjects(lt.isCanOverride())
.build();
}
@@ -304,51 +221,49 @@
* Create a "submit requirement" that is only satisfied if the label is voted with the max votes
* and/or not voted by the min vote, according to the label attributes.
*/
- private static SubmitRequirement createBlockingOrRequiredSr(
- String labelName, LabelAttributes attributes) {
+ private static SubmitRequirement createBlockingOrRequiredSr(LabelType lt) {
SubmitRequirement.Builder builder =
SubmitRequirement.builder()
- .setName(labelName)
- .setAllowOverrideInChildProjects(attributes.canOverride());
+ .setName(lt.getName())
+ .setAllowOverrideInChildProjects(lt.isCanOverride());
String maxPart =
- String.format("label:%s=MAX", labelName)
- + (attributes.ignoreSelfApproval() ? ",user=non_uploader" : "");
- switch (attributes.function()) {
- case "MaxWithBlock" ->
+ String.format("label:%s=MAX", lt.getName())
+ + (lt.isIgnoreSelfApproval() ? ",user=non_uploader" : "");
+ switch (lt.getFunction()) {
+ case MAX_WITH_BLOCK ->
builder.setSubmittabilityExpression(
SubmitRequirementExpression.create(
- String.format("%s AND -label:%s=MIN", maxPart, labelName)));
- case "AnyWithBlock" ->
+ String.format("%s AND -label:%s=MIN", maxPart, lt.getName())));
+ case ANY_WITH_BLOCK ->
builder.setSubmittabilityExpression(
- SubmitRequirementExpression.create(String.format("-label:%s=MIN", labelName)));
- case "MaxNoBlock" ->
+ SubmitRequirementExpression.create(String.format("-label:%s=MIN", lt.getName())));
+ case MAX_NO_BLOCK ->
builder.setSubmittabilityExpression(SubmitRequirementExpression.create(maxPart));
default -> {}
}
- if (!attributes.refPatterns().isEmpty()) {
+ ImmutableList<String> refPatterns = lt.getRefPatterns();
+ if (refPatterns != null && !refPatterns.isEmpty()) {
builder.setApplicabilityExpression(
SubmitRequirementExpression.of(
String.join(
" OR ",
- attributes.refPatterns().stream()
+ lt.getRefPatterns().stream()
.map(b -> "branch:\\\"" + b + "\\\"")
.collect(Collectors.toList()))));
}
return builder.build();
}
- private static boolean isBlockingOrRequiredLabel(String function) {
- return function.equals("AnyWithBlock")
- || function.equals("MaxWithBlock")
- || function.equals("MaxNoBlock");
+ private static boolean isBlockingOrRequiredLabel(LabelType lt) {
+ return switch (lt.getFunction()) {
+ case ANY_WITH_BLOCK, MAX_WITH_BLOCK, MAX_NO_BLOCK -> true;
+ case NO_BLOCK, NO_OP, PATCH_SET_LOCK -> false;
+ };
}
- /**
- * Returns true if the label definition was skipped in the project, i.e. it had only one defined
- * value: zero.
- */
- private static boolean isLabelSkipped(List<String> values) {
- return values.isEmpty() || (values.size() == 1 && values.get(0).startsWith("0"));
+ private static boolean isLabelSkipped(LabelType lt) {
+ ImmutableList<LabelValue> values = lt.getValues();
+ return values.isEmpty() || (values.size() == 1 && values.get(0).getValue() == 0);
}
public boolean anyProjectHasProlog(Collection<Project.NameKey> allProjects) throws IOException {
@@ -380,85 +295,53 @@
}
}
- /**
- * Returns a map containing submit requirement names in lower name as keys, with {@link
- * com.google.gerrit.entities.SubmitRequirement} as value.
- */
- private Map<String, SubmitRequirement> loadSubmitRequirements(Config rc) {
- Map<String, SubmitRequirement> allRequirements = new LinkedHashMap<>();
- for (String name : rc.getSubsections(ProjectConfig.SUBMIT_REQUIREMENT)) {
- String description =
- rc.getString(ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_DESCRIPTION);
- String applicabilityExpr =
- rc.getString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- name,
- ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION);
- String submittabilityExpr =
- rc.getString(
- ProjectConfig.SUBMIT_REQUIREMENT,
- name,
- ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION);
- String overrideExpr =
- rc.getString(
- ProjectConfig.SUBMIT_REQUIREMENT, name, ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION);
- boolean canInherit =
- rc.getBoolean(
- ProjectConfig.SUBMIT_REQUIREMENT,
- name,
- ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
- false);
- SubmitRequirement submitRequirement =
- SubmitRequirement.builder()
- .setName(name)
- .setDescription(Optional.ofNullable(description))
- .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
- .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
- .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
- .setAllowOverrideInChildProjects(canInherit)
- .build();
- allRequirements.put(name.toLowerCase(Locale.ROOT), submitRequirement);
- }
- return allRequirements;
- }
-
- private static boolean hasMigrationAlreadyRun(Repository repo) throws IOException {
- try (RevWalk revWalk = new RevWalk(repo)) {
- Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
- if (refsMetaConfig == null) {
+ private boolean hasMigrationAlreadyRun(Project.NameKey project) throws IOException {
+ try (Repository repo = repoManager.openRepository(project)) {
+ try (RevWalk revWalk = new RevWalk(repo)) {
+ Ref refsMetaConfig = repo.exactRef(RefNames.REFS_CONFIG);
+ if (refsMetaConfig == null) {
+ return false;
+ }
+ revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
+ RevCommit commit;
+ while ((commit = revWalk.next()) != null) {
+ if (COMMIT_MSG.equals(commit.getShortMessage())) {
+ return true;
+ }
+ }
return false;
}
- revWalk.markStart(revWalk.parseCommit(refsMetaConfig.getObjectId()));
- RevCommit commit;
- while ((commit = revWalk.next()) != null) {
- if (COMMIT_MSG.equals(commit.getShortMessage())) {
- return true;
- }
- }
- return false;
}
}
- @AutoValue
- abstract static class LabelAttributes {
- abstract String function();
+ /**
+ * Helper "Map" to of submit requirements with case-preserving keys and case-insensitive lookup
+ */
+ private static class SubmitRequirementMap {
+ private final Map<String, SubmitRequirement> submitRequirements;
+ private final Map<String, String> lowerCaseToOriginalNames;
- abstract boolean canOverride();
+ SubmitRequirementMap(Map<String, SubmitRequirement> submitRequirements) {
+ this.submitRequirements = submitRequirements;
+ this.lowerCaseToOriginalNames =
+ submitRequirements.keySet().stream()
+ .collect(Collectors.toMap(k -> k.toLowerCase(Locale.ROOT), k -> k));
+ }
- abstract boolean ignoreSelfApproval();
+ boolean containsKey(String name) {
+ return lowerCaseToOriginalNames.containsKey(name.toLowerCase(Locale.ROOT));
+ }
- abstract ImmutableList<String> values();
+ @Nullable
+ SubmitRequirement get(String name) {
+ String orig = lowerCaseToOriginalNames.get(name.toLowerCase(Locale.ROOT));
+ return orig != null ? submitRequirements.get(orig) : null;
+ }
- abstract ImmutableList<String> refPatterns();
-
- static LabelAttributes create(
- String function,
- boolean canOverride,
- boolean ignoreSelfApproval,
- ImmutableList<String> values,
- ImmutableList<String> refPatterns) {
- return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes(
- function, canOverride, ignoreSelfApproval, values, refPatterns);
+ void put(SubmitRequirement sr) {
+ String name = sr.name();
+ submitRequirements.put(name, sr);
+ lowerCaseToOriginalNames.put(name.toLowerCase(Locale.ROOT), name);
}
}
}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 8e1b86e..7e237f5 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -446,7 +446,7 @@
private TestUpdateUI runMigration(Status expectedResult) throws Exception {
TestUpdateUI updateUi = new TestUpdateUI();
MigrateLabelFunctionsToSubmitRequirement executor =
- new MigrateLabelFunctionsToSubmitRequirement(repoManager, serverIdent.get());
+ new MigrateLabelFunctionsToSubmitRequirement(repoMetaDataUpdater, repoManager);
Status status = executor.executeMigration(project, updateUi);
assertThat(status).isEqualTo(expectedResult);
projectCache.evictAndReindex(project);
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index ac51959..fc7bfba 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -233,6 +233,12 @@
yarn eslint
```
+* To run ESLint and apply changes on the whole app:
+
+```sh
+yarn eslintfix
+```
+
* To run ESLint on just the subdirectory you modified:
```sh
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index ed85fe2..7695b60 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -33,6 +33,7 @@
// Fires GerritView values such as 'change', 'dashboard', 'admin', ...
VIEW_CHANGE = 'view-change',
SHOW_REVISION_ACTIONS = 'show-revision-actions',
+ BEFORE_COMMIT_MSG_EDIT = 'before-commit-msg-edit',
COMMIT_MSG_EDIT = 'commitmsgedit',
CUSTOM_EMOJIS = 'custom-emojis',
REVERT = 'revert',
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 29fdb92..9b06622 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -360,7 +360,7 @@
<label class="selectionLabel">
<md-checkbox
?checked=${this.checked}
- @click=${this.toggleCheckbox}
+ @change=${this.toggleCheckbox}
></md-checkbox>
</label>
</td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index bd53670..d8c1a5d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -296,7 +296,7 @@
class="selection-checkbox"
?checked=${checked}
.indeterminate=${indeterminate}
- @click=${this.handleSelectAllCheckboxClicked}
+ @change=${this.handleSelectAllCheckboxClicked}
></md-checkbox>
</td>
`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 216559c..0393242 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -54,6 +54,7 @@
RequestPayload,
RevertSubmissionInfo,
ReviewInput,
+ RevisionInfo,
} from '../../../types/common';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
@@ -1361,7 +1362,10 @@
}
// private but used in test
- getRevision(change: ChangeInfo, patchNum?: PatchSetNumber) {
+ getRevision(
+ change: ChangeInfo,
+ patchNum?: PatchSetNumber
+ ): RevisionInfo | null {
for (const rev of Object.values(change.revisions ?? {})) {
if (rev._number === patchNum) {
return rev;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 942a77f..934fd65 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1658,7 +1658,7 @@
}
// Private but used in tests.
- handleCommitMessageSave(e: EditableContentSaveEvent) {
+ async handleCommitMessageSave(e: EditableContentSaveEvent) {
assertIsDefined(this.change, 'change');
assertIsDefined(this.changeNum, 'changeNum');
// to prevent 2 requests at the same time
@@ -1667,6 +1667,19 @@
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
const committerEmail = e.detail.committerEmail;
+ // The BEFORE event handlers are async and can potentially block
+ // the message edit from going through. By contrast, the second
+ // set of event handlers always fire (and should probably fire
+ // after the message has been saved successfully, but the current
+ // behavior is what it is).
+ if (
+ !(await this.getPluginLoader().jsApiService.handleBeforeCommitMessage(
+ this.change,
+ message
+ ))
+ )
+ return;
+
this.getPluginLoader().jsApiService.handleCommitMessage(
this.change,
message
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 825fa3e..0409f80 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -1044,15 +1044,17 @@
});
assertIsDefined(element.commitMessageEditor);
- element.handleCommitMessageSave(
+ await element.handleCommitMessageSave(
mockEvent('test \n test ', committerEmail)
);
assert.equal(putStub.lastCall.args[1], 'test\n test');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(mockEvent(' test\ntest', committerEmail));
+ await element.handleCommitMessageSave(
+ mockEvent(' test\ntest', committerEmail)
+ );
assert.equal(putStub.lastCall.args[1], ' test\ntest');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(
+ await element.handleCommitMessageSave(
mockEvent('\n\n\n\n\n\n\n\n', committerEmail)
);
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 623fe40..04c8618 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -1425,6 +1425,7 @@
);
if (!this.ccs.find(cc => getUserId(cc) === getUserId(accountToMove))) {
this.ccs = [...this.ccs, accountToMove];
+ this.reviewersMutated = true;
}
} else {
this.ccs = this.ccs.filter(
@@ -1434,6 +1435,7 @@
!this.reviewers.find(r => getUserId(r) === getUserId(accountToMove))
) {
this.reviewers = [...this.reviewers, accountToMove];
+ this.reviewersMutated = true;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
index de37852..6fdb74e 100644
--- a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
+++ b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
@@ -67,7 +67,7 @@
class="selectionLabel"
id=${option.name}
?checked=${!!this.isOptionSelected.get(option.name)}
- @click=${() => this.toggleCheckbox(option)}
+ @change=${() => this.toggleCheckbox(option)}
></md-checkbox>
<label for=${option.name}
>${capitalizeFirstLetter(option.description)}</label
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 6471d1e..f348a80 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -10,9 +10,12 @@
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-default-editor/gr-default-editor';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {
Base64FileContent,
+ ChangeInfo,
EditPreferencesInfo,
+ RevisionInfo,
RevisionPatchSetNum,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
@@ -94,6 +97,8 @@
private readonly reporting = getAppContext().reportingService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
private readonly getStorage = resolve(this, storageServiceToken);
private readonly getUserModel = resolve(this, userModelToken);
@@ -541,11 +546,27 @@
return;
}
assertIsDefined(this.change, 'change');
+
+ this.getPluginLoader().jsApiService.handlePublishEdit(
+ this.change as ChangeInfo,
+ this.getLatestRevision(this.change as ChangeInfo)
+ );
+
this.getChangeModel().navigateToChangeResetReload();
});
});
};
+ private getLatestRevision(change: ChangeInfo): RevisionInfo | null {
+ const patchNum = this.latestPatchsetNumber;
+ for (const rev of Object.values(change.revisions ?? {})) {
+ if (rev._number === patchNum) {
+ return rev;
+ }
+ }
+ return null;
+ }
+
private handleContentChange(e: CustomEvent<{value: string}>) {
this.storeTask = debounce(
this.storeTask,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 9e970e6..8fb6e88 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -82,7 +82,7 @@
id="numberCheckbox"
name="number"
?checked=${!!this.showNumber}
- @click=${this.handleNumberCheckboxClick}
+ @change=${this.handleNumberCheckboxClick}
></md-checkbox>
</td>
</tr>
@@ -100,7 +100,7 @@
id=${column}
name=${column}
?checked=${!this.computeIsColumnHidden(column)}
- @click=${this.handleTargetClick}
+ @change=${this.handleTargetClick}
></md-checkbox>
</td>
</tr>`;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index b322d46..a0a747d 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -160,7 +160,7 @@
assert.equal(element.displayedColumns.length, displayedLength - 1);
});
- test('show item', async () => {
+ test('show and hide item', async () => {
element.displayedColumns = [
ColumnNames.STATUS,
ColumnNames.OWNER,
@@ -171,20 +171,35 @@
// trigger computation of enabled displayed columns
element.serverConfig = createServerInfo();
await element.updateComplete;
- const checkbox = queryAndAssert<MdCheckbox>(
+
+ const checkboxSubject = queryAndAssert<MdCheckbox>(
element,
'table tr:nth-child(2) md-checkbox'
);
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isFalse(isChecked);
- const table = queryAndAssert<HTMLTableElement>(element, 'table');
- assert.equal(table.style.display, '');
+ assert.equal(checkboxSubject.name, 'Subject');
+ const checkboxOwner = queryAndAssert<MdCheckbox>(
+ element,
+ 'table tr:nth-child(3) md-checkbox'
+ );
+ assert.equal(checkboxOwner.name, 'Owner');
- checkbox.click();
+ assert.equal(element.displayedColumns.length, 5);
+ assert.isFalse(checkboxSubject.checked);
+ assert.isTrue(checkboxOwner.checked);
+
+ checkboxSubject.click();
await element.updateComplete;
- assert.equal(element.displayedColumns.length, displayedLength + 1);
+ assert.equal(element.displayedColumns.length, 6);
+ assert.isTrue(checkboxSubject.checked);
+ assert.isTrue(checkboxOwner.checked);
+
+ checkboxOwner.click();
+ await element.updateComplete;
+
+ assert.equal(element.displayedColumns.length, 5);
+ assert.isTrue(checkboxSubject.checked);
+ assert.isFalse(checkboxOwner.checked);
});
test('getDisplayedColumns', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 56f5f55..df26cb8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -880,7 +880,7 @@
<md-checkbox
class="show-hide"
?checked=${!!this.collapsed}
- @click=${() => (this.collapsed = !this.collapsed)}
+ @change=${() => (this.collapsed = !this.collapsed)}
></md-checkbox>
<gr-icon icon=${icon} id="icon"></gr-icon>
</label>
@@ -1031,7 +1031,7 @@
<md-checkbox
id="resolvedCheckbox"
?checked=${!this.unresolved}
- @click=${this.handleToggleResolved}
+ @change=${this.handleToggleResolved}
></md-checkbox>
Resolved
</label>
@@ -1182,7 +1182,7 @@
<md-checkbox
id="generateSuggestCheckbox"
?checked=${this.generateSuggestion}
- @click=${() => {
+ @change=${() => {
this.generateSuggestion = !this.generateSuggestion;
// Reset so suggestion can be re-generated.
this.wasSuggestionEdited = false;
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index 1e02b29..7c8bcf4 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -366,7 +366,7 @@
<md-checkbox
class="show-hide"
?checked=${this.collapsed}
- @click=${() => {
+ @change=${() => {
this.collapsed = !this.collapsed;
if (this.collapsed) {
this.reporting.reportInteraction(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 6d8496d..8417a94 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -195,6 +195,23 @@
}
}
+ async handleBeforeCommitMessage(
+ change: ChangeInfo | ParsedChangeInfo,
+ msg: string
+ ): Promise<boolean> {
+ let okay = true;
+ for (const cb of this._getEventCallbacks(
+ EventType.BEFORE_COMMIT_MSG_EDIT
+ )) {
+ try {
+ okay = (await cb(change, msg)) && okay;
+ } catch (err: unknown) {
+ this.reportError(err, EventType.BEFORE_COMMIT_MSG_EDIT);
+ }
+ }
+ return okay;
+ }
+
handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string) {
for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index a000456..9f40f7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -70,6 +70,17 @@
): string;
addElement(key: TargetElement, el: HTMLElement): void;
getAdminMenuLinks(): MenuLink[];
+ /**
+ * This method is called before handling a commit message edit.
+ * It allows plugins to conditionally block edits.
+ * @param change The relevant change.
+ * @param msg The new commit message text.
+ * @return A promise that resolves to true if the action should proceed.
+ */
+ handleBeforeCommitMessage(
+ change: ChangeInfo | ParsedChangeInfo,
+ msg: string
+ ): Promise<boolean>;
handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string): void;
canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null): boolean;
getReviewPostRevert(change?: ChangeInfo): ReviewInput;
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 5577075..a71d225 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -21,7 +21,14 @@
RevisionPatchSetNum,
} from '../../types/common';
import {ChangeStatus, DefaultBase} from '../../constants/constants';
-import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatest,
+ forkJoin,
+ from,
+ Observable,
+ of,
+} from 'rxjs';
import {
catchError,
filter,
@@ -52,6 +59,7 @@
import {Model} from '../base/model';
import {UserModel} from '../user/user-model';
import {define} from '../dependency';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
import {
isOwner,
isUploader,
@@ -351,6 +359,10 @@
private latestPatchNum?: PatchSetNumber;
+ private readonly reloadSubmittabilityTrigger$ = new BehaviorSubject<void>(
+ undefined
+ );
+
public readonly change$ = select(
this.state$,
changeState => changeState.change
@@ -520,7 +532,8 @@
private readonly restApiService: RestApiService,
private readonly userModel: UserModel,
private readonly pluginLoader: PluginLoader,
- private readonly reporting: ReportingService
+ private readonly reporting: ReportingService,
+ private readonly flagsService: FlagsService
) {
super(initialState);
this.patchNum$ = select(
@@ -798,14 +811,22 @@
.subscribe(mergeable => this.updateState({mergeable}));
}
+ public reloadSubmittability() {
+ this.reloadSubmittabilityTrigger$.next();
+ }
+
private loadSubmittabilityInfo() {
// Use the same trigger as loadChange, to run SR loading in parallel.
- return this.viewModel.changeNum$
+ return combineLatest([
+ this.viewModel.changeNum$,
+ this.reloadSubmittabilityTrigger$,
+ ])
.pipe(
+ map(([changeNum, _]) => changeNum),
switchMap(changeNum => {
if (!changeNum) {
- // On reload changeNum is set to undefined to reset change state.
- // We propagate undefined and reset the state in this case.
+ // On change reload changeNum is set to undefined to reset change
+ // state. We propagate undefined and reset the state in this case.
return of(undefined);
}
return from(this.restApiService.getSubmittabilityInfo(changeNum));
@@ -814,7 +835,11 @@
.subscribe(submittabilityInfo => {
// TODO(b/445644919): Remove once the submit_requirements is never
// requested as part of the change detail.
- if (this.change?.submit_requirements) {
+ if (
+ !this.flagsService.isEnabled(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ )
+ ) {
return;
}
const change = fillFromSubmittabilityInfo(
@@ -1102,19 +1127,11 @@
change = updateRevisionsWithCommitShas(change);
// TODO(b/445644919): Remove once the submit_requirements is never requested
// as part of the change detail.
- if (change!.submit_requirements) {
- this.updateState({
- change,
- submittabilityInfo: {
- changeNum: change!._number,
- submittable: change!.submittable!,
- submitRequirements: change!.submit_requirements,
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- return;
+ if (
+ this.flagsService.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)
+ ) {
+ change = fillFromSubmittabilityInfo(change, this.submittabilityInfo);
}
- change = fillFromSubmittabilityInfo(change, this.submittabilityInfo);
this.updateState({
change,
loadingStatus: LoadingStatus.LOADED,
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index a58a070..ce3533e 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -62,6 +62,7 @@
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {ShowChangeDetail} from '../../elements/shared/gr-js-api-interface/gr-js-api-types';
import {SubmittabilityInfo} from '../../services/gr-rest-api/gr-rest-api';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
suite('updateRevisionsWithCommitShas() tests', () => {
test('undefined edit', async () => {
@@ -221,11 +222,26 @@
});
});
+class TestFlagService implements FlagsService {
+ public experiments: Set<string> = new Set();
+
+ finalize() {}
+
+ isEnabled(experimentId: string): boolean {
+ return this.experiments.has(experimentId);
+ }
+
+ get enabledExperiments() {
+ return [...this.experiments];
+ }
+}
+
suite('change model tests', () => {
let changeViewModel: ChangeViewModel;
let changeModel: ChangeModel;
let knownChange: ParsedChangeInfo;
let knownChangeNoRevision: ChangeInfo;
+ let testFlagService: TestFlagService;
const testCompleted = new Subject<void>();
async function waitForLoadingStatus(
@@ -239,6 +255,7 @@
}
setup(() => {
+ testFlagService = new TestFlagService();
changeViewModel = testResolver(changeViewModelToken);
changeModel = new ChangeModel(
testResolver(navigationToken),
@@ -246,7 +263,8 @@
getAppContext().restApiService,
testResolver(userModelToken),
testResolver(pluginLoaderToken),
- getAppContext().reportingService
+ getAppContext().reportingService,
+ testFlagService
);
knownChangeNoRevision = {
...createChange(),
@@ -490,6 +508,9 @@
});
test('load submit requirements (SRs load first)', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
const stubDetail = stubRestApi('getChangeDetail').callsFake(
() => promiseDetail
@@ -520,6 +541,9 @@
});
test('load submit requirements (Detail load first, experiment enabled)', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
const stubDetail = stubRestApi('getChangeDetail').callsFake(
() => promiseDetail
@@ -550,7 +574,7 @@
assert.equal(stubSrs.callCount, 1);
});
- test('load submit requirements (Detail load first, experiment disabled)', async () => {
+ test('load submit requirements (experiment disabled)', async () => {
const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
const stubDetail = stubRestApi('getChangeDetail').callsFake(
() => promiseDetail
@@ -559,25 +583,54 @@
const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
() => promiseSrs
);
- let state: ChangeState;
testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseSrs.resolve(undefined);
promiseDetail.resolve({
...knownChange,
submittable: false,
submit_requirements: [createSubmitRequirementResultInfo()],
});
- state = await waitForLoadingStatus(LoadingStatus.LOADED);
- promiseSrs.resolve(undefined);
- state = await waitUntilObserved(
+ const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ // Validate that submit requirements didn't get reset to undefined.
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('reload submit requirements', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
+ // Set initial state
+ const stubDetail = stubRestApi('getChangeDetail').resolves(knownChange);
+ const stubSrs = stubRestApi('getSubmittabilityInfo');
+ stubSrs.resolves({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ await waitUntilObserved(
changeModel.state$,
state => state.submittabilityInfo !== undefined,
'SubmitRequirements was never loaded'
);
- // Validate that submit requirements didn't get reset to undefined.
- assert.isTrue(state.submittabilityInfo?.submittable === false);
- assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ await waitForLoadingStatus(LoadingStatus.LOADED);
+ stubSrs.resolves({
+ changeNum: knownChange._number,
+ submittable: true,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ changeModel.reloadSubmittability();
+ const state = await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo?.submittable === true,
+ 'Submittability never reloaded'
+ );
+ assert.isTrue(state.change?.submittable);
assert.equal(stubDetail.callCount, 1);
- assert.equal(stubSrs.callCount, 1);
+ assert.equal(stubSrs.callCount, 2);
});
test('navigating to another change', async () => {
diff --git a/polygerrit-ui/app/models/flows/flows-model.ts b/polygerrit-ui/app/models/flows/flows-model.ts
index 70f0661..460cec6 100644
--- a/polygerrit-ui/app/models/flows/flows-model.ts
+++ b/polygerrit-ui/app/models/flows/flows-model.ts
@@ -12,6 +12,7 @@
import {NumericChangeId} from '../../types/common';
import {getAppContext} from '../../services/app-context';
+import {KnownExperimentId} from '../../services/flags/flags';
export interface FlowsState {
flows: FlowInfo[];
@@ -32,6 +33,8 @@
private readonly restApiService = getAppContext().restApiService;
+ private flagService = getAppContext().flagsService;
+
constructor(private readonly changeModel: ChangeModel) {
super({
flows: [],
@@ -48,7 +51,11 @@
combineLatest([this.changeModel.changeNum$, this.reload$])
.pipe(
switchMap(([changeNum]) => {
- if (!changeNum) return of([]);
+ if (
+ !changeNum ||
+ !this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB)
+ )
+ return of([]);
this.setState({...this.getState(), loading: true});
return from(this.restApiService.listFlows(changeNum)).pipe(
catchError(err => {
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 24d37a8..a0ab3a3 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -161,7 +161,8 @@
appContext.restApiService,
resolver(userModelToken),
resolver(pluginLoaderToken),
- appContext.reportingService
+ appContext.reportingService,
+ appContext.flagsService
),
],
[