diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart index eebcc29520a..421b9020e29 100644 --- a/packages/flutter_tools/lib/src/commands/upgrade.dart +++ b/packages/flutter_tools/lib/src/commands/upgrade.dart @@ -116,7 +116,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'); @@ -225,10 +225,79 @@ class UpgradeCommandRunner { } } + /// Checks if the Flutter git repository is tracking a standard remote. + /// + /// A "standard remote" is one having the same url as [globals.flutterGit]. + /// The upgrade process only supports standard remotes. + /// + /// Exits tool if the tracking remote is not standard. + void verifyStandardRemote(FlutterVersion localVersion) { + // If repositoryUrl of the local version is null, exit + if (localVersion.repositoryUrl == null) { + throwToolExit( + 'Unable to upgrade Flutter: The tool could not determine the remote ' + 'upstream which is being tracked by the SDK.\n' + 'Re-install Flutter by going to $_flutterInstallDocs.' + ); + } + + // Strip `.git` suffix before comparing the remotes + final String trackingUrl = stripDotGit(localVersion.repositoryUrl); + final String flutterGitUrl = stripDotGit(globals.flutterGit); + + // Exempt the official flutter 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 ' + '"${globals.flutterGit}".\n' + 'Either remove "FLUTTER_GIT_URL" from the environment or set it 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 remain 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 HEAD isn't pointing to a branch, or there is no upstream. - Future fetchLatestVersion() async { + Future fetchLatestVersion({ + @required FlutterVersion localVersion, + }) async { String revision; try { // Fetch upstream branch's commits and tags @@ -263,6 +332,7 @@ class UpgradeCommandRunner { 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 a3c93ae4a3e..c21a69e17bc 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart @@ -173,7 +173,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); @@ -199,7 +199,7 @@ void main() { ]); await expectLater( - () async => realCommandRunner.fetchLatestVersion(), + () async => realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion()), throwsToolExit(message: 'Unable to upgrade Flutter: Your Flutter checkout ' 'is currently not on a release branch.\n' 'Use "flutter channel" to switch to an official channel, and retry. ' @@ -228,7 +228,7 @@ void main() { ]); await expectLater( - () async => realCommandRunner.fetchLatestVersion(), + () async => realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion()), throwsToolExit(message: 'Unable to upgrade Flutter: The current Flutter ' 'branch/channel is not tracking any remote repository.\n' 'Re-install Flutter by going to https://flutter.dev/docs/get-started/install.' @@ -240,6 +240,131 @@ void main() { Platform: () => fakePlatform, }); + group('verifyStandardRemote', () { + const String flutterStandardUrlDotGit = 'https://github.com/flutter/flutter.git'; + const String flutterNonStandardUrlDotGit = 'https://githubmirror.com/flutter/flutter.git'; + const String flutterStandardSshUrl = 'git@github.com:flutter/flutter'; + + testUsingContext('throws toolExit if repository url is null', () async { + final FakeFlutterVersion flutterVersion = FakeFlutterVersion( + channel: 'dev', + repositoryUrl: null, + ); + + await expectLater( + () async => realCommandRunner.verifyStandardRemote(flutterVersion), + throwsToolExit(message: 'Unable to upgrade Flutter: The tool could not ' + 'determine the remote upstream which is being tracked by the SDK.\n' + '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 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('throws toolExit at non-standard remote url with FLUTTER_GIT_URL unset', () async { + final FakeFlutterVersion flutterVersion = FakeFlutterVersion( + channel: 'dev', + repositoryUrl: flutterNonStandardUrlDotGit, + ); + + await expectLater( + () async => realCommandRunner.verifyStandardRemote(flutterVersion), + throwsToolExit(message: 'Unable to upgrade Flutter: The Flutter SDK ' + 'is tracking a non-standard remote "$flutterNonStandardUrlDotGit".\n' + 'Set the environment variable "FLUTTER_GIT_URL" to ' + '"$flutterNonStandardUrlDotGit", and retry. ' + 'Alternatively, re-install Flutter by going to ' + 'https://flutter.dev/docs/get-started/install.\n' + 'If this is intentional, 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, + ); + + await expectLater( + () async => realCommandRunner.verifyStandardRemote(flutterVersion), + throwsToolExit(message: 'Unable to upgrade Flutter: The Flutter SDK ' + 'is tracking "$flutterNonStandardUrlDotGit" but "FLUTTER_GIT_URL" ' + 'is set to "$flutterStandardUrlDotGit".\n' + 'Either remove "FLUTTER_GIT_URL" from the environment or set it to ' + '"$flutterNonStandardUrlDotGit", and retry. ' + 'Alternatively, re-install Flutter by going to ' + 'https://flutter.dev/docs/get-started/install.\n' + 'If this is intentional, 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': flutterStandardUrlDotGit, + }), + }); + + testUsingContext('exempts standard ssh url from check with FLUTTER_GIT_URL unset', () async { + final FakeFlutterVersion flutterVersion = FakeFlutterVersion( + channel: 'dev', + repositoryUrl: flutterStandardSshUrl, + ); + + expect(() => realCommandRunner.verifyStandardRemote(flutterVersion), returnsNormally); + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + ProcessManager: () => processManager, + Platform: () => fakePlatform, + }); + + testUsingContext('stripDotGit removes ".git" suffix if any', () async { + expect(realCommandRunner.stripDotGit('https://github.com/flutter/flutter.git'), 'https://github.com/flutter/flutter'); + expect(realCommandRunner.stripDotGit('https://github.com/flutter/flutter'), 'https://github.com/flutter/flutter'); + expect(realCommandRunner.stripDotGit('git@github.com:flutter/flutter.git'), 'git@github.com:flutter/flutter'); + expect(realCommandRunner.stripDotGit('git@github.com:flutter/flutter'), 'git@github.com:flutter/flutter'); + expect(realCommandRunner.stripDotGit('https://githubmirror.com/flutter/flutter.git.git'), 'https://githubmirror.com/flutter/flutter.git'); + expect(realCommandRunner.stripDotGit('https://githubmirror.com/flutter/flutter.gitgit'), 'https://githubmirror.com/flutter/flutter.gitgit'); + }); + }); + testUsingContext('git exception during attemptReset throwsToolExit', () async { const String revision = 'abc123'; const String errorMessage = 'fatal: Could not parse object ´$revision´'; @@ -475,7 +600,7 @@ class FakeUpgradeCommandRunner extends UpgradeCommandRunner { FlutterVersion remoteVersion; @override - Future fetchLatestVersion() async => remoteVersion; + Future fetchLatestVersion({FlutterVersion localVersion}) async => remoteVersion; @override Future hasUncommittedChanges() async => willHaveUncommittedChanges;