[flutter_tools] Make `flutter upgrade` only work with standard remotes (#79372)

diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index 8112485..0dbfd1b 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -17,6 +17,12 @@
 import '../runner/flutter_command.dart';
 import '../version.dart';
 
+/// The flutter GitHub repository.
+String get _flutterGit => globals.platform.environment['FLUTTER_GIT_URL'] ?? 'https://github.com/flutter/flutter.git';
+
+/// The official docs to install Flutter.
+String get _flutterInstallDocs => 'https://flutter.dev/docs/get-started/install';
+
 class UpgradeCommand extends FlutterCommand {
   UpgradeCommand({
     @required bool verboseHelp,
@@ -113,7 +119,7 @@
     @required bool testFlow,
     @required bool verifyOnly,
   }) async {
-    final FlutterVersion upstreamVersion = await fetchLatestVersion();
+    final FlutterVersion upstreamVersion = await fetchLatestVersion(localVersion: flutterVersion);
     if (flutterVersion.frameworkRevision == upstreamVersion.frameworkRevision) {
       globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
       globals.printStatus('$flutterVersion');
@@ -222,10 +228,80 @@
     }
   }
 
+  /// Checks if the Flutter git repository is tracking a "standard remote".
+  ///
+  /// Using `flutter upgrade` is not supported from a non-standard remote. A git
+  /// remote should have the same url as [_flutterGit] to be considered as a
+  /// "standard" remote.
+  ///
+  /// Exits tool if the tracking remote is not standard.
+  void verifyStandardRemote(FlutterVersion localVersion) {
+    // If localVersion.repositoryUrl is null, exit
+    if (localVersion.repositoryUrl == null) {
+      throwToolExit(
+        'Unable to upgrade Flutter: The tool could not determine the url of '
+        'the remote upstream which is currently being tracked by the SDK.\n'
+        'Re-install Flutter by going to $_flutterInstallDocs.'
+      );
+    }
+
+    // Strip `.git` suffix from repository url and _flutterGit
+    final String trackingUrl = _stripDotGit(localVersion.repositoryUrl);
+    final String flutterGitUrl = _stripDotGit(_flutterGit);
+
+    // Exempt the flutter GitHub git SSH remote from this check
+    if (trackingUrl == 'git@github.com:flutter/flutter') {
+      return;
+    }
+
+    if (trackingUrl != flutterGitUrl) {
+      if (globals.platform.environment.containsKey('FLUTTER_GIT_URL')) {
+        // If `FLUTTER_GIT_URL` is set, inform the user to either remove the
+        // `FLUTTER_GIT_URL` environment variable or set it to the current
+        // tracking remote to continue.
+        throwToolExit(
+          'Unable to upgrade Flutter: The Flutter SDK is tracking '
+          '"${localVersion.repositoryUrl}" but "FLUTTER_GIT_URL" is set to '
+          '"$_flutterGit".\n'
+          'Either remove "FLUTTER_GIT_URL" from the environment or set '
+          '"FLUTTER_GIT_URL" to "${localVersion.repositoryUrl}", and retry. '
+          'Alternatively, re-install Flutter by going to $_flutterInstallDocs.\n'
+          'If this is intentional, it is recommended to use "git" directly to '
+          'keep Flutter SDK up-to date.'
+        );
+      }
+      // If `FLUTTER_GIT_URL` is unset, inform that the user has to set the
+      // environment variable to continue.
+      throwToolExit(
+        'Unable to upgrade Flutter: The Flutter SDK is tracking a non-standard '
+        'remote "${localVersion.repositoryUrl}".\n'
+        'Set the environment variable "FLUTTER_GIT_URL" to '
+        '"${localVersion.repositoryUrl}", and retry. '
+        'Alternatively, re-install Flutter by going to $_flutterInstallDocs.\n'
+        'If this is intentional, it is recommended to use "git" directly to '
+        'keep Flutter SDK up-to date.'
+      );
+    }
+  }
+
+  // Strips ".git" suffix from a given string, preferably an url.
+  // For example, changes 'https://github.com/flutter/flutter.git' to 'https://github.com/flutter/flutter'.
+  // URLs without ".git" suffix will be unaffected.
+  String _stripDotGit(String url) {
+    final RegExp pattern = RegExp(r'(.*)(\.git)$');
+    final RegExpMatch match = pattern.firstMatch(url);
+    if (match == null) {
+      return url;
+    }
+    return match.group(1);
+  }
+
   /// Returns the remote HEAD flutter version.
   ///
-  /// Exits tool if there is no upstream.
-  Future<FlutterVersion> fetchLatestVersion() async {
+  /// Exits tool if HEAD isn't pointing to a branch, or there is no upstream.
+  Future<FlutterVersion> fetchLatestVersion({
+    @required FlutterVersion localVersion,
+  }) async {
     String revision;
     try {
       // Fetch upstream branch's commits and tags
@@ -245,20 +321,22 @@
       final String errorString = e.toString();
       if (errorString.contains('fatal: HEAD does not point to a branch')) {
         throwToolExit(
-          'You are not currently on a release branch. Use git to '
-          "check out an official branch ('stable', 'beta', 'dev', or 'master') "
-          'and retry, for example:\n'
-          '  git checkout stable'
+          'Unable to upgrade Flutter: HEAD does not point to a branch (Are you '
+          'in a detached HEAD state?).\n'
+          'Use "flutter channel" to switch to an official channel, and retry. '
+          'Alternatively, re-install Flutter by going to $_flutterInstallDocs.'
         );
       } else if (errorString.contains('fatal: no upstream configured for branch')) {
         throwToolExit(
-          'Unable to upgrade Flutter: no origin repository configured. '
-          "Run 'git remote add origin "
-          "https://github.com/flutter/flutter' in $workingDirectory");
+          'Unable to upgrade Flutter: No upstream repository configured for '
+          'current branch.\n'
+          'Re-install Flutter by going to $_flutterInstallDocs.'
+        );
       } else {
         throwToolExit(errorString);
       }
     }
+    verifyStandardRemote(localVersion);
     return FlutterVersion(workingDirectory: workingDirectory, frameworkRevision: revision);
   }
 
diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
index d6ec1c1..4e74fa6 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -4,6 +4,7 @@
 
 // @dart = 2.8
 
+import 'package:flutter_tools/src/base/common.dart' show ToolExit;
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/platform.dart';
@@ -173,7 +174,7 @@
         stdout: version),
       ]);
 
-      final FlutterVersion updateVersion = await realCommandRunner.fetchLatestVersion();
+      final FlutterVersion updateVersion = await realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion());
 
       expect(updateVersion.frameworkVersion, version);
       expect(updateVersion.frameworkRevision, revision);
@@ -198,17 +199,23 @@
         ),
       ]);
 
-      await expectLater(
-            () async => realCommandRunner.fetchLatestVersion(),
-        throwsToolExit(message: 'You are not currently on a release branch.'),
-      );
+      ToolExit err;
+      try {
+        await realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion());
+      } on ToolExit catch (e) {
+        err = e;
+      }
+      expect(err, isNotNull);
+      expect(err.toString(), contains('Unable to upgrade Flutter: HEAD does not point to a branch'));
+      expect(err.toString(), contains('Use "flutter channel" to switch to an official channel, and retry'));
+      expect(err.toString(), contains('re-install Flutter by going to https://flutter.dev/docs/get-started/install'));
       expect(processManager, hasNoRemainingExpectations);
     }, overrides: <Type, Generator>{
       ProcessManager: () => processManager,
       Platform: () => fakePlatform,
     });
 
-    testUsingContext('fetchRemoteRevision throws toolExit if no upstream configured', () async {
+    testUsingContext('fetchLatestVersion throws toolExit if no upstream configured', () async {
       processManager.addCommands(const <FakeCommand>[
         FakeCommand(command: <String>[
           'git', 'fetch', '--tags'
@@ -223,18 +230,152 @@
         ),
       ]);
 
-      await expectLater(
-            () async => realCommandRunner.fetchLatestVersion(),
-        throwsToolExit(
-          message: 'Unable to upgrade Flutter: no origin repository configured.',
-        ),
-      );
+      ToolExit err;
+      try {
+        await realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion());
+      } on ToolExit catch (e) {
+        err = e;
+      }
+      expect(err, isNotNull);
+      expect(err.toString(), contains('Unable to upgrade Flutter: No upstream repository configured for current branch.'));
+      expect(err.toString(), contains('Re-install Flutter by going to https://flutter.dev/docs/get-started/install'));
       expect(processManager, hasNoRemainingExpectations);
     }, overrides: <Type, Generator>{
       ProcessManager: () => processManager,
       Platform: () => fakePlatform,
     });
 
+    group('verifyStandardRemote', () {
+      const String flutterStandardUrlDotGit = 'https://github.com/flutter/flutter.git';
+      const String flutterNonStandardUrlDotGit = 'https://githubmirror.com/flutter/flutter.git';
+      const String flutterStandardUrl = 'https://github.com/flutter/flutter';
+      const String flutterStandardSshUrlDotGit = 'git@github.com:flutter/flutter.git';
+
+      testUsingContext('throws toolExit if repository url is null', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: null,
+        );
+
+        ToolExit err;
+        try {
+         realCommandRunner.verifyStandardRemote(flutterVersion);
+        } on ToolExit catch (e) {
+          err = e;
+        }
+        expect(err, isNotNull);
+        expect(err.toString(), contains('could not determine the url of the remote upstream which is currently being tracked by the SDK'));
+        expect(err.toString(), contains('Re-install Flutter by going to https://flutter.dev/docs/get-started/install'));
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('does not throw toolExit at standard remote url with .git suffix and FLUTTER_GIT_URL unset', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterStandardUrlDotGit,
+        );
+
+        expect(() => realCommandRunner.verifyStandardRemote(flutterVersion), returnsNormally);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('does not throw toolExit at standard remote url without .git suffix and FLUTTER_GIT_URL unset', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterStandardUrl,
+        );
+
+        expect(() => realCommandRunner.verifyStandardRemote(flutterVersion), returnsNormally);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('throws toolExit at non-standard remote url with FLUTTER_GIT_URL unset', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterNonStandardUrlDotGit,
+        );
+
+        ToolExit err;
+        try {
+         realCommandRunner.verifyStandardRemote(flutterVersion);
+        } on ToolExit catch (e) {
+          err = e;
+        }
+        expect(err, isNotNull);
+        expect(err.toString(), contains('The Flutter SDK is tracking a non-standard remote "$flutterNonStandardUrlDotGit"'));
+        expect(err.toString(), contains('Set the environment variable "FLUTTER_GIT_URL" to "$flutterNonStandardUrlDotGit", and retry.'));
+        expect(err.toString(), contains('re-install Flutter by going to https://flutter.dev/docs/get-started/install'));
+        expect(err.toString(), contains('it is recommended to use "git" directly to keep Flutter SDK up-to date.'));
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('does not throw toolExit at non-standard remote url with FLUTTER_GIT_URL set', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterNonStandardUrlDotGit,
+        );
+
+        expect(() => realCommandRunner.verifyStandardRemote(flutterVersion), returnsNormally);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform..environment = Map<String, String>.unmodifiable(<String, String> {
+          'FLUTTER_GIT_URL': flutterNonStandardUrlDotGit,
+        }),
+      });
+
+      testUsingContext('throws toolExit at remote url and FLUTTER_GIT_URL set to different urls', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterNonStandardUrlDotGit,
+        );
+
+        ToolExit err;
+        try {
+         realCommandRunner.verifyStandardRemote(flutterVersion);
+        } on ToolExit catch (e) {
+          err = e;
+        }
+        expect(err, isNotNull);
+        expect(err.toString(), contains('The Flutter SDK is tracking "$flutterNonStandardUrlDotGit"'));
+        expect(err.toString(), contains('but "FLUTTER_GIT_URL" is set to "$flutterStandardUrl"'));
+        expect(err.toString(), contains('remove "FLUTTER_GIT_URL" from the environment or set "FLUTTER_GIT_URL" to "$flutterNonStandardUrlDotGit"'));
+        expect(err.toString(), contains('re-install Flutter by going to https://flutter.dev/docs/get-started/install'));
+        expect(err.toString(), contains('it is recommended to use "git" directly to keep Flutter SDK up-to date.'));
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform..environment = Map<String, String>.unmodifiable(<String, String> {
+          'FLUTTER_GIT_URL': flutterStandardUrl,
+        }),
+      });
+
+      testUsingContext('exempts standard ssh url from check with FLUTTER_GIT_URL unset', () async {
+        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
+          channel: 'dev',
+          repositoryUrl: flutterStandardSshUrlDotGit,
+        );
+
+        expect(() => realCommandRunner.verifyStandardRemote(flutterVersion), returnsNormally);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator> {
+        ProcessManager: () => processManager,
+        Platform: () => fakePlatform,
+      });
+    });
+
     testUsingContext('git exception during attemptReset throwsToolExit', () async {
       const String revision = 'abc123';
       const String errorMessage = 'fatal: Could not parse object ´$revision´';
@@ -470,7 +611,7 @@
   FlutterVersion remoteVersion;
 
   @override
-  Future<FlutterVersion> fetchLatestVersion() async => remoteVersion;
+  Future<FlutterVersion> fetchLatestVersion({FlutterVersion localVersion}) async => remoteVersion;
 
   @override
   Future<bool> hasUncommittedChanges() async => willHaveUncommittedChanges;