Merge "Add isFlowsEnabled API"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e0dee26..a12e1ce 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3722,10 +3722,37 @@
   HTTP/1.1 204 No Content
 ----
 
-
 [[flow-endpoints]]
 == Flow Endpoints
 
+
+[[is-flows-enabled]]
+=== Is Flows Enabled
+--
+'GET /changes/link:#change-id[\{change-id\}]/is-flows-enabled'
+--
+
+Returns whether flows are enabled for this change.
+
+As result an link:#enabled-info[IsFlowsEnabledInfo] entity is returned.
+
+.Request
+----
+  GET /changes/myProject~65178/is-flows-enabled HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "enabled": true
+  }
+----
+
 [[list-flows]]
 === List Flows
 --
@@ -8322,6 +8349,16 @@
 link:#rebase-edit[rebases the change edit] and conflicts are allowed.
 |===========================
 
+[[enabled-info]]
+=== IsFlowsEnabledInfo
+The `IsFlowsEnabledInfo` entity contains information about whether flows is enabled.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`enabled`     ||A boolean indicating whether the feature is enabled.
+|===========================
+
 [[evaluate-change-query-expression-result-info]]
 === EvaluateChangeQueryExpressionResultInfo
 The `EvaluateChangeQueryExpressionResultInfo` entity contains the result of
diff --git a/java/com/google/gerrit/acceptance/TestExtensions.java b/java/com/google/gerrit/acceptance/TestExtensions.java
index cd6b5ee..97fcf0e 100644
--- a/java/com/google/gerrit/acceptance/TestExtensions.java
+++ b/java/com/google/gerrit/acceptance/TestExtensions.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PluginPushOption;
 import com.google.gerrit.server.ValidationOptionsListener;
@@ -234,6 +235,13 @@
     }
 
     @Override
+    public Boolean isFlowsEnabled(Project.NameKey projectName, Change.Id changeId)
+        throws RestApiException {
+      // Always return true for testing purposes.
+      return true;
+    }
+
+    @Override
     public Optional<Flow> getFlow(FlowKey flowKey) throws StorageException {
       return Optional.ofNullable(flows.get(flowKey));
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 6d7b800..7af343a 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
 import com.google.gerrit.extensions.common.FlowInfo;
 import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RebaseChainInfo;
@@ -103,6 +104,8 @@
   /** Look up a flow of this change by its UUID. */
   FlowApi flow(String flowUuid) throws RestApiException;
 
+  IsFlowsEnabledInfo isFlowsEnabled() throws RestApiException;
+
   /** Get the flows of this change/ */
   List<FlowInfo> flows() throws RestApiException;
 
diff --git a/java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java b/java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java
new file mode 100644
index 0000000..21e4734
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java
@@ -0,0 +1,28 @@
+// 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.extensions.common;
+
+/**
+ * Representation of a boolean flag in the REST API.
+ *
+ * <p>This class determines the JSON format of boolean flags in the REST API.
+ */
+public class IsFlowsEnabledInfo {
+  public boolean enabled;
+
+  public IsFlowsEnabledInfo(boolean enabled) {
+    this.enabled = enabled;
+  }
+}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d3bcbfc..9ce3d79 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.extensions.common.FlowInput;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.InputWithMessage;
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RebaseChainInfo;
@@ -119,6 +120,7 @@
 import com.google.gerrit.server.restapi.change.SuggestChangeReviewers;
 import com.google.gerrit.server.restapi.flow.CreateFlow;
 import com.google.gerrit.server.restapi.flow.FlowCollection;
+import com.google.gerrit.server.restapi.flow.IsFlowsEnabled;
 import com.google.gerrit.server.restapi.flow.ListFlows;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.Inject;
@@ -188,6 +190,7 @@
   private final PutMessage putMessage;
   private final CreateFlow createFlow;
   private final ListFlows listFlows;
+  private final IsFlowsEnabled isFlowsEnabled;
   private final Provider<EvaluateChangeQueryExpression> evaluateChangeQueryExpressionProvider;
   private final Provider<GetPureRevert> getPureRevertProvider;
   private final DynamicOptionParser dynamicOptionParser;
@@ -246,6 +249,7 @@
       PutMessage putMessage,
       CreateFlow createFlow,
       ListFlows listFlows,
+      IsFlowsEnabled isFlowsEnabled,
       Provider<EvaluateChangeQueryExpression> evaluateChangeQueryExpressionProvider,
       Provider<GetPureRevert> getPureRevertProvider,
       DynamicOptionParser dynamicOptionParser,
@@ -302,6 +306,7 @@
     this.putMessage = putMessage;
     this.createFlow = createFlow;
     this.listFlows = listFlows;
+    this.isFlowsEnabled = isFlowsEnabled;
     this.evaluateChangeQueryExpressionProvider = evaluateChangeQueryExpressionProvider;
     this.getPureRevertProvider = getPureRevertProvider;
     this.dynamicOptionParser = dynamicOptionParser;
@@ -352,6 +357,15 @@
   }
 
   @Override
+  public IsFlowsEnabledInfo isFlowsEnabled() throws RestApiException {
+    try {
+      return isFlowsEnabled.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check if flows are enabled", e);
+    }
+  }
+
+  @Override
   public List<FlowInfo> flows() throws RestApiException {
     try {
       return listFlows.apply(change).value();
diff --git a/java/com/google/gerrit/server/flow/FlowService.java b/java/com/google/gerrit/server/flow/FlowService.java
index 32d8b56..25fd799 100644
--- a/java/com/google/gerrit/server/flow/FlowService.java
+++ b/java/com/google/gerrit/server/flow/FlowService.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import java.util.Optional;
 
 /**
@@ -34,6 +35,21 @@
  */
 @ExtensionPoint
 public interface FlowService {
+
+  /**
+   * Checks if Flows is enabled for change.
+   *
+   * <p>Can be used to disable flows at a change/project level. Implementations can have user
+   * information injected to disable it for a specific user.
+   *
+   * @param projectName The name of the project that contains the change.
+   * @param changeId The ID of the change for which the flows should be listed.
+   * @return If flows is enabled for the user.
+   * @throws RestApiException thrown if checking flow access has failed
+   */
+  public Boolean isFlowsEnabled(Project.NameKey projectName, Change.Id changeId)
+      throws RestApiException;
+
   /**
    * Create a new flow.
    *
diff --git a/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java b/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java
index bcc241f..442e6b1 100644
--- a/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java
@@ -33,5 +33,7 @@
 
     get(FLOW_KIND).to(GetFlow.class);
     delete(FLOW_KIND).to(DeleteFlow.class);
+
+    get(CHANGE_KIND, "is-flows-enabled").to(IsFlowsEnabled.class);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
new file mode 100644
index 0000000..941e87e
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
@@ -0,0 +1,49 @@
+// 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.flow;
+
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * REST endpoint to check if Flows is enabled for the user.
+ *
+ * <p>This REST endpoint handles {@code GET /change/<change-id>/isFlowsEnabled} requests.
+ */
+@Singleton
+public class IsFlowsEnabled implements RestReadView<ChangeResource> {
+  private final FlowServiceUtil flowServiceUtil;
+
+  @Inject
+  IsFlowsEnabled(FlowServiceUtil flowServiceUtil) {
+    this.flowServiceUtil = flowServiceUtil;
+  }
+
+  @Override
+  public Response<IsFlowsEnabledInfo> apply(ChangeResource changeResource) throws RestApiException {
+    IsFlowsEnabledInfo enabledInfo =
+        new IsFlowsEnabledInfo(
+            flowServiceUtil
+                .getFlowServiceOrThrow()
+                .isFlowsEnabled(changeResource.getProject(), changeResource.getId()));
+    return Response.ok(enabledInfo);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java
index f568886..f5953d2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java
@@ -40,7 +40,10 @@
   @Inject private ExtensionRegistry extensionRegistry;
 
   private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/changes/%s/flows"), RestCall.post("/changes/%s/flows"));
+      ImmutableList.of(
+          RestCall.get("/changes/%s/flows"),
+          RestCall.get("/changes/%s/is-flows-enabled"),
+          RestCall.post("/changes/%s/flows"));
 
   private static final ImmutableList<RestCall> FLOW_ENDPOINTS =
       ImmutableList.of(