// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:process/process.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/terminal.dart'; import '../cache.dart'; import '../globals.dart' as globals; import '../persistent_tool_state.dart'; import '../runner/flutter_command.dart'; import '../version.dart'; /// The flutter downgrade command returns the SDK to the last recorded version /// for a particular branch. /// /// For example, suppose a user on the beta channel upgrades from 1.2.3 to 1.4.6. /// The tool will record that sha "abcdefg" was the last active beta channel in the /// persistent tool state. If the user is still on the beta channel and runs /// flutter downgrade, this will take the user back to "abcdefg". They will not be /// able to downgrade again, since the tool only records one prior version. /// Additionally, if they had switched channels to stable before trying to downgrade, /// the command would fail since there was no previously recorded stable version. class DowngradeCommand extends FlutterCommand { DowngradeCommand({ bool verboseHelp = false, PersistentToolState? persistentToolState, required Logger logger, ProcessManager? processManager, FlutterVersion? flutterVersion, Terminal? terminal, Stdio? stdio, FileSystem? fileSystem, }) : _terminal = terminal, _flutterVersion = flutterVersion, _persistentToolState = persistentToolState, _processManager = processManager, _stdio = stdio, _logger = logger, _fileSystem = fileSystem { argParser.addOption( 'working-directory', hide: !verboseHelp, help: 'Override the downgrade working directory. ' 'This is only intended to enable integration testing of the tool itself.' ); argParser.addFlag( 'prompt', defaultsTo: true, hide: !verboseHelp, help: 'Show the downgrade prompt. ' 'The ability to disable this using "--no-prompt" is only provided for ' 'integration testing of the tool itself.' ); } Terminal? _terminal; FlutterVersion? _flutterVersion; PersistentToolState? _persistentToolState; ProcessUtils? _processUtils; ProcessManager? _processManager; final Logger _logger; Stdio? _stdio; FileSystem? _fileSystem; @override String get description => 'Downgrade Flutter to the last active version for the current channel.'; @override String get name => 'downgrade'; @override final String category = FlutterCommandCategory.sdk; @override Future<FlutterCommandResult> runCommand() async { // Commands do not necessarily have access to the correct zone injected // values when being created. Fields must be lazily instantiated in runCommand, // at least until the zone injection is refactored. _terminal ??= globals.terminal; _flutterVersion ??= globals.flutterVersion; _persistentToolState ??= globals.persistentToolState; _processManager ??= globals.processManager; _processUtils ??= ProcessUtils(processManager: _processManager!, logger: _logger); _stdio ??= globals.stdio; _fileSystem ??= globals.fs; String workingDirectory = Cache.flutterRoot!; if (argResults!.wasParsed('working-directory')) { workingDirectory = stringArgDeprecated('working-directory')!; _flutterVersion = FlutterVersion(workingDirectory: workingDirectory); } final String currentChannel = _flutterVersion!.channel; final Channel? channel = getChannelForName(currentChannel); if (channel == null) { throwToolExit( 'Flutter is not currently on a known channel. Use "flutter channel <name>" ' 'to switch to an official channel.', ); } final PersistentToolState persistentToolState = _persistentToolState!; final String? lastFlutterVersion = persistentToolState.lastActiveVersion(channel); final String? currentFlutterVersion = _flutterVersion?.frameworkRevision; if (lastFlutterVersion == null || currentFlutterVersion == lastFlutterVersion) { final String trailing = await _createErrorMessage(workingDirectory, channel); throwToolExit( 'There is no previously recorded version for channel "$currentChannel".\n' '$trailing' ); } // Detect unknown versions. final ProcessUtils processUtils = _processUtils!; final RunResult parseResult = await processUtils.run(<String>[ 'git', 'describe', '--tags', lastFlutterVersion, ], workingDirectory: workingDirectory); if (parseResult.exitCode != 0) { throwToolExit('Failed to parse version for downgrade:\n${parseResult.stderr}'); } final String humanReadableVersion = parseResult.stdout; // If there is a terminal attached, prompt the user to confirm the downgrade. final Stdio stdio = _stdio!; final Terminal terminal = _terminal!; if (stdio.hasTerminal && boolArgDeprecated('prompt')) { terminal.usesTerminalUi = true; final String result = await terminal.promptForCharInput( const <String>['y', 'n'], prompt: 'Downgrade flutter to version $humanReadableVersion?', logger: _logger, ); if (result == 'n') { return FlutterCommandResult.success(); } } else { _logger.printStatus('Downgrading Flutter to version $humanReadableVersion'); } // To downgrade the tool, we perform a git checkout --hard, and then // switch channels. The version recorded must have existed on that branch // so this operation is safe. try { await processUtils.run( <String>['git', 'reset', '--hard', lastFlutterVersion], throwOnError: true, workingDirectory: workingDirectory, ); } on ProcessException catch (error) { throwToolExit( 'Unable to downgrade Flutter: The tool could not update to the version ' '$humanReadableVersion. This may be due to git not being installed or an ' 'internal error. Please ensure that git is installed on your computer and ' 'retry again.\nError: $error.' ); } try { await processUtils.run( <String>['git', 'checkout', currentChannel, '--'], throwOnError: true, workingDirectory: workingDirectory, ); } on ProcessException catch (error) { throwToolExit( 'Unable to downgrade Flutter: The tool could not switch to the channel ' '$currentChannel. This may be due to git not being installed or an ' 'internal error. Please ensure that git is installed on your computer ' 'and retry again.\nError: $error.' ); } await FlutterVersion.resetFlutterVersionFreshnessCheck(); _logger.printStatus('Success'); return FlutterCommandResult.success(); } // Formats an error message that lists the currently stored versions. Future<String> _createErrorMessage(String workingDirectory, Channel currentChannel) async { final StringBuffer buffer = StringBuffer(); for (final Channel channel in Channel.values) { if (channel == currentChannel) { continue; } final String? sha = _persistentToolState?.lastActiveVersion(channel); if (sha == null) { continue; } final RunResult parseResult = await _processUtils!.run(<String>[ 'git', 'describe', '--tags', sha, ], workingDirectory: workingDirectory); if (parseResult.exitCode == 0) { buffer.writeln('Channel "${getNameForChannel(channel)}" was previously on: ${parseResult.stdout}.'); } } return buffer.toString(); } }