// 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 '../cache.dart'; import '../globals.dart' as globals; import '../runner/flutter_command.dart'; import '../version.dart'; 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, ); } @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: boolArgDeprecated('all'), verbose: globalResults?['verbose'] == 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; final String currentBranch = globals.flutterVersion.getBranchName(); final Set<String> seenUnofficialChannels = <String>{}; final List<String> rawOutput = <String>[]; showAll = showAll || currentChannel != currentBranch; 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 List<String> officialChannels = kOfficialChannels.toList(); final List<bool> availableChannels = List<bool>.filled(officialChannels.length, false); 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 (split.length > 1) { final int index = officialChannels.indexOf(branch); if (index != -1) { // Mark all available channels official channels from output availableChannels[index] = true; } else if (showAll && !seenUnofficialChannels.contains(branch)) { // add other branches to seenUnofficialChannels if --all flag is given (to print later) seenUnofficialChannels.add(branch); } } } // print all available official channels in sorted manner for (int i = 0; i < officialChannels.length; i++) { // only print non-missing channels if (availableChannels[i]) { String currentIndicator = ' '; if (officialChannels[i] == currentChannel) { currentIndicator = '*'; } globals.printStatus('$currentIndicator ${officialChannels[i]}'); } } // 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'); } } } if (currentChannel == 'unknown') { 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); 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. int result = await globals.processUtils.stream( <String>['git', 'fetch'], workingDirectory: Cache.flutterRoot, prefix: 'git: ', ); if (result == 0) { result = await globals.processUtils.stream( <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/$branchName'], workingDirectory: Cache.flutterRoot, prefix: 'git: ', ); if (result == 0) { // branch already exists, try just switching to it result = await globals.processUtils.stream( <String>['git', 'checkout', branchName, '--'], workingDirectory: Cache.flutterRoot, prefix: 'git: ', ); } else { // branch does not exist, we have to create it result = await globals.processUtils.stream( <String>['git', 'checkout', '--track', '-b', branchName, 'origin/$branchName'], workingDirectory: Cache.flutterRoot, prefix: 'git: ', ); } } if (result != 0) { throwToolExit('Switching channels failed with error code $result.', exitCode: result); } else { // Remove the version check stamp, since it could contain out-of-date // information that pertains to the previous channel. await FlutterVersion.resetFlutterVersionFreshnessCheck(); } } }