Allow disabling experimental commands, devices on stable branch (#30153)

diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
index 42ce098..0bb0d9b 100644
--- a/packages/flutter_tools/lib/src/commands/build_web.dart
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -32,6 +32,9 @@
   bool get hidden => true;
 
   @override
+  bool get isExperimental => true;
+
+  @override
   final String description = '(EXPERIMENTAL) build a web application bundle.';
 
   @override
diff --git a/packages/flutter_tools/lib/src/desktop.dart b/packages/flutter_tools/lib/src/desktop.dart
index 153e2fb..e1af669 100644
--- a/packages/flutter_tools/lib/src/desktop.dart
+++ b/packages/flutter_tools/lib/src/desktop.dart
@@ -3,11 +3,12 @@
 // found in the LICENSE file.
 
 import 'base/platform.dart';
+import 'version.dart';
 
 // Only launch or display desktop embedding devices if
 // `FLUTTER_DESKTOP_EMBEDDING` environment variable is set to true.
 bool get flutterDesktopEnabled {
   _flutterDesktopEnabled ??= platform.environment['FLUTTER_DESKTOP_EMBEDDING']?.toLowerCase() == 'true';
-  return _flutterDesktopEnabled;
+  return _flutterDesktopEnabled && !FlutterVersion.instance.isStable;
 }
 bool _flutterDesktopEnabled;
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 4341b1d..dd84802 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -27,6 +27,7 @@
 import '../globals.dart';
 import '../project.dart';
 import '../usage.dart';
+import '../version.dart';
 import 'flutter_command_runner.dart';
 
 export '../cache.dart' show DevelopmentArtifact;
@@ -464,6 +465,11 @@
     }
   }
 
+  /// Whether this feature should not be usable on stable branches.
+  ///
+  /// Defaults to false, meaning it is usable.
+  bool get isExperimental => false;
+
   /// Additional usage values to be sent with the usage ping.
   Future<Map<String, String>> get usageValues async => const <String, String>{};
 
@@ -635,6 +641,12 @@
   @protected
   @mustCallSuper
   Future<void> validateCommand() async {
+    // If we're on a stable branch, then don't allow the usage of
+    // "experimental" features.
+    if (isExperimental && FlutterVersion.instance.isStable) {
+      throwToolExit('Experimental feature $name is not supported on stable branches');
+    }
+
     if (_requiresPubspecYaml && !PackageMap.isUsingCustomPackagesPath) {
       // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
       if (!fs.isFileSync('pubspec.yaml')) {
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index 3df1df0..d571f37 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -32,6 +32,11 @@
     return _repositoryUrl;
   }
 
+  /// Whether we are currently on the stable branch.
+  bool get isStable {
+    return getBranchName() == 'stable';
+  }
+
   static const Set<String> officialChannels = <String>{
     'master',
     'dev',
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 4b34393..fd78729 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -13,6 +13,7 @@
 import '../device.dart';
 import '../globals.dart';
 import '../project.dart';
+import '../version.dart';
 import '../web/compile.dart';
 
 ChromeLauncher get chromeLauncher => context[ChromeLauncher];
@@ -21,7 +22,7 @@
 /// environment variable is set to true.
 bool get flutterWebEnabled {
   _flutterWebEnabled = platform.environment['FLUTTER_WEB']?.toLowerCase() == 'true';
-  return _flutterWebEnabled;
+  return _flutterWebEnabled && !FlutterVersion.instance.isStable;
 }
 bool _flutterWebEnabled;
 
diff --git a/packages/flutter_tools/test/runner/flutter_command_test.dart b/packages/flutter_tools/test/runner/flutter_command_test.dart
index e1cfc2a..e91b95b 100644
--- a/packages/flutter_tools/test/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/runner/flutter_command_test.dart
@@ -7,6 +7,7 @@
 import 'package:flutter_tools/src/usage.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
@@ -155,4 +156,43 @@
     });
 
   });
+
+  group('Experimental commands', () {
+    final MockVersion stableVersion = MockVersion();
+    final MockVersion betaVersion = MockVersion();
+    final FakeCommand fakeCommand = FakeCommand();
+    when(stableVersion.isStable).thenReturn(true);
+    when(betaVersion.isStable).thenReturn(false);
+
+    testUsingContext('Can be disabled on stable branch', () async {
+      expect(() => fakeCommand.run(), throwsA(isA<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => stableVersion,
+    });
+
+    testUsingContext('Works normally on regular branches', () async {
+      expect(fakeCommand.run(), completes);
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => betaVersion,
+    });
+  });
 }
+
+
+class FakeCommand extends FlutterCommand {
+  @override
+  String get description => null;
+
+  @override
+  String get name => 'fake';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    return null;
+  }
+}
+
+class MockVersion extends Mock implements FlutterVersion {}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index d89d322..7203676 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -304,7 +304,10 @@
   }
 }
 
-class MockFlutterVersion extends Mock implements FlutterVersion {}
+class MockFlutterVersion extends Mock implements FlutterVersion {
+  @override
+  bool get isStable => false;
+}
 
 class MockClock extends Mock implements SystemClock {}