// 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 '../base/common.dart'; import '../base/process.dart'; import '../cache.dart'; import '../globals.dart' as globals; import '../runner/flutter_command.dart'; import '../runner/flutter_command_runner.dart'; import '../version.dart'; import 'upgrade.dart' show precacheArtifacts; class ChannelCommand extends FlutterCommand { ChannelCommand({ bool verboseHelp = false }) { argParser.addFlag( 'all', abbr: 'a', help: 'Include all the available branches (including local branches) when listing channels.', hide: !verboseHelp, ); argParser.addFlag( 'cache-artifacts', help: 'After switching channels, download all required binary artifacts. ' 'This is the equivalent of running "flutter precache" with the "--all-platforms" flag.', defaultsTo: true, ); } @override final String name = 'channel'; @override final String description = 'List or switch Flutter channels.'; @override final String category = FlutterCommandCategory.sdk; @override String get invocation => '${runner?.executableName} $name [<channel-name>]'; @override Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{}; @override Future<FlutterCommandResult> runCommand() async { final List<String> rest = argResults?.rest ?? <String>[]; switch (rest.length) { case 0: await _listChannels( showAll: boolArg('all'), verbose: globalResults?[FlutterGlobalOptions.kVerboseFlag] == true, ); return FlutterCommandResult.success(); case 1: await _switchChannel(rest[0]); return FlutterCommandResult.success(); default: throw ToolExit('Too many arguments.\n$usage'); } } Future<void> _listChannels({ required bool showAll, required bool verbose }) async { // Beware: currentBranch could contain PII. See getBranchName(). final String currentChannel = globals.flutterVersion.channel; // limited to known branch names assert(kOfficialChannels.contains(currentChannel) || kObsoleteBranches.containsKey(currentChannel) || currentChannel == kUserBranch, 'potential PII leak in channel name: "$currentChannel"'); final String currentBranch = globals.flutterVersion.getBranchName(); final Set<String> seenUnofficialChannels = <String>{}; final List<String> rawOutput = <String>[]; globals.printStatus('Flutter channels:'); final int result = await globals.processUtils.stream( <String>['git', 'branch', '-r'], workingDirectory: Cache.flutterRoot, mapFunction: (String line) { rawOutput.add(line); return null; }, ); if (result != 0) { final String details = verbose ? '\n${rawOutput.join('\n')}' : ''; throwToolExit('List channels failed: $result$details', exitCode: result); } final Set<String> availableChannels = <String>{}; for (final String line in rawOutput) { final List<String> split = line.split('/'); if (split.length != 2) { // We don't know how to parse this line, skip it. continue; } final String branch = split[1]; if (kOfficialChannels.contains(branch)) { availableChannels.add(branch); } else if (showAll) { seenUnofficialChannels.add(branch); } } bool currentChannelIsOfficial = false; // print all available official channels in sorted manner for (final String channel in kOfficialChannels) { // only print non-missing channels if (availableChannels.contains(channel)) { String currentIndicator = ' '; if (channel == currentChannel) { currentIndicator = '*'; currentChannelIsOfficial = true; } globals.printStatus('$currentIndicator $channel (${kChannelDescriptions[channel]})'); } } // print all remaining channels if showAll is true if (showAll) { for (final String branch in seenUnofficialChannels) { if (currentBranch == branch) { globals.printStatus('* $branch'); } else if (!branch.startsWith('HEAD ')) { globals.printStatus(' $branch'); } } } else if (!currentChannelIsOfficial) { globals.printStatus('* $currentBranch'); } if (!currentChannelIsOfficial) { assert(currentChannel == kUserBranch, 'Current channel is "$currentChannel", which is not an official branch. (Current branch is "$currentBranch".)'); globals.printStatus(''); globals.printStatus('Currently not on an official channel.'); } } Future<void> _switchChannel(String branchName) async { globals.printStatus("Switching to flutter channel '$branchName'..."); if (kObsoleteBranches.containsKey(branchName)) { final String alternative = kObsoleteBranches[branchName]!; globals.printStatus("This channel is obsolete. Consider switching to the '$alternative' channel instead."); } else if (!kOfficialChannels.contains(branchName)) { globals.printStatus('This is not an official channel. For a list of available channels, try "flutter channel".'); } await _checkout(branchName); if (boolArg('cache-artifacts')) { await precacheArtifacts(Cache.flutterRoot); } globals.printStatus("Successfully switched to flutter channel '$branchName'."); globals.printStatus("To ensure that you're on the latest build from this channel, run 'flutter upgrade'"); } static Future<void> upgradeChannel(FlutterVersion currentVersion) async { final String channel = currentVersion.channel; if (kObsoleteBranches.containsKey(channel)) { final String alternative = kObsoleteBranches[channel]!; globals.printStatus("Transitioning from '$channel' to '$alternative'..."); return _checkout(alternative); } } static Future<void> _checkout(String branchName) async { // Get latest refs from upstream. RunResult runResult = await globals.processUtils.run( <String>['git', 'fetch'], workingDirectory: Cache.flutterRoot, ); if (runResult.processResult.exitCode == 0) { runResult = await globals.processUtils.run( <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/$branchName'], workingDirectory: Cache.flutterRoot, ); if (runResult.processResult.exitCode == 0) { // branch already exists, try just switching to it runResult = await globals.processUtils.run( <String>['git', 'checkout', branchName, '--'], workingDirectory: Cache.flutterRoot, ); } else { // branch does not exist, we have to create it runResult = await globals.processUtils.run( <String>['git', 'checkout', '--track', '-b', branchName, 'origin/$branchName'], workingDirectory: Cache.flutterRoot, ); } } if (runResult.processResult.exitCode != 0) { throwToolExit( 'Switching channels failed\n$runResult.', exitCode: runResult.processResult.exitCode, ); } else { // Remove the version check stamp, since it could contain out-of-date // information that pertains to the previous channel. await FlutterVersion.resetFlutterVersionFreshnessCheck(); } } }