From c835ad43625e2506d5887987e404ab43e2e594a6 Mon Sep 17 00:00:00 2001 From: Anurag Roy <44899587+RoyARG02@users.noreply.github.com> Date: Wed, 26 May 2021 01:49:03 +0530 Subject: [PATCH] [flutter_tools] Make `flutter upgrade` only work with standard remotes (#79372) --- .../lib/src/commands/upgrade.dart | 98 ++++++++-- .../permeable/upgrade_test.dart | 167 ++++++++++++++++-- 2 files changed, 242 insertions(+), 23 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart index 811248517c6..0dbfd1b6b37 100644 --- a/packages/flutter_tools/lib/src/commands/upgrade.dart +++ b/packages/flutter_tools/lib/src/commands/upgrade.dart @@ -17,6 +17,12 @@ import '../globals_null_migrated.dart' as globals; 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 @@ class UpgradeCommandRunner { @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 @@ class UpgradeCommandRunner { } } + /// 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 fetchLatestVersion() async { + /// Exits tool if HEAD isn't pointing to a branch, or there is no upstream. + Future fetchLatestVersion({ + @required FlutterVersion localVersion, + }) async { String revision; try { // Fetch upstream branch's commits and tags @@ -245,20 +321,22 @@ class UpgradeCommandRunner { 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 d6ec1c116ff..4e74fa61e67 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 @@ void main() { 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 @@ void main() { ), ]); - 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: { 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(command: [ 'git', 'fetch', '--tags' @@ -223,18 +230,152 @@ void main() { ), ]); - 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: { 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: { + 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: { + 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: { + 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: { + 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: { + ProcessManager: () => processManager, + Platform: () => fakePlatform..environment = Map.unmodifiable( { + '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: { + ProcessManager: () => processManager, + Platform: () => fakePlatform..environment = Map.unmodifiable( { + '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: { + 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 @@ class FakeUpgradeCommandRunner extends UpgradeCommandRunner { FlutterVersion remoteVersion; @override - Future fetchLatestVersion() async => remoteVersion; + Future fetchLatestVersion({FlutterVersion localVersion}) async => remoteVersion; @override Future hasUncommittedChanges() async => willHaveUncommittedChanges;