[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;