New release process (#14061)

Generate the "version" file from git tags.
Remove the old VERSION file and mentions of versions in pubspec.yaml files.
Replace the old update_versions.dart script with a new roll_dev.dart script.
Update "flutter channel".
Update "flutter upgrade", including making it transition from alpha to dev.
Update "flutter --version" and "flutter doctor".
diff --git a/.gitignore b/.gitignore
index 5d0e2f0..e8f27b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@
 /dev/docs/lib/
 /dev/docs/pubspec.yaml
 /packages/flutter/coverage/
+version
 
 # Flutter/Dart/Pub related
 **/doc/api/
diff --git a/.travis.yml b/.travis.yml
index d3f9d9c..51dccfd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,21 @@
+# ENVIRONMENTS
 os:
   - linux
   - osx
+env:
+  - SHARD=analyze
+  - SHARD=tests
+  - SHARD=docs
+matrix:
+  exclude:
+  - os: osx
+    env: SHARD=analyze
+  - os: osx
+    env: SHARD=docs
 sudo: false
 filter_secrets: false
+
+# INSTALLATION
 addons:
   apt:
     # sky_shell binary depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18
@@ -14,23 +27,18 @@
 language: node_js
 node_js:
   - "6"
+git:
+  # We rely on git tags for determining the version.
+  depth: false
+cache:
+  directories:
+    - $HOME/.pub-cache
 install:
   - ./dev/bots/travis_install.sh
-env:
-  - SHARD=analyze
-  - SHARD=tests
-  - SHARD=docs
+
+# TESTING
 before_script:
   - ./dev/bots/travis_setup.sh
 script:
   - ulimit -S -n 2048 # https://github.com/flutter/flutter/issues/2976
   - (./bin/cache/dart-sdk/bin/dart ./dev/bots/test.dart && ./dev/bots/travis_upload.sh)
-cache:
-  directories:
-    - $HOME/.pub-cache
-matrix:
-  exclude:
-  - os: osx
-    env: SHARD=analyze
-  - os: osx
-    env: SHARD=docs
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 5fbe99d..0000000
--- a/VERSION
+++ /dev/null
@@ -1,9 +0,0 @@
-# This file defines a semantic version for the Flutter SDK. This version will
-# describes breaking changes in the SDK as a whole, separately from the
-# individual packages in the SDK. For example, if the flutter command line
-# tool's options change in an incompatible way, this version will update to
-# reflect that change. However, if the API for package:flutter changes in an
-# incompatible way, this version number might not change. Instead, the version
-# number for package:flutter will update to reflect that change.
-
-0.0.21-dev
diff --git a/bin/flutter b/bin/flutter
index d068fcb..d6b6072 100755
--- a/bin/flutter
+++ b/bin/flutter
@@ -73,6 +73,7 @@
 
   local revision=`(cd "$FLUTTER_ROOT"; git rev-parse HEAD)`
   if [ ! -f "$SNAPSHOT_PATH" ] || [ ! -s "$STAMP_PATH" ] || [ `cat "$STAMP_PATH"` != "$revision" ] || [ "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]; then
+    rm -f "$FLUTTER_ROOT/version"
     mkdir -p "$FLUTTER_ROOT/bin/cache"
     touch "$FLUTTER_ROOT/bin/cache/.dartignore"
     "$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"
diff --git a/bin/flutter.bat b/bin/flutter.bat
index f69b277..da43e49 100644
--- a/bin/flutter.bat
+++ b/bin/flutter.bat
@@ -93,6 +93,7 @@
     )
 
   :do_snapshot
+    IF EXIST "%FLUTTER_ROOT%\version" DEL "%FLUTTER_ROOT%\version"
     ECHO: > "%cache_dir%\.dartignore"
     ECHO Updating flutter tool...
     PUSHD "%flutter_tools_dir%"
diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml
index 20370b1..4929f3a 100644
--- a/dev/devicelab/pubspec.yaml
+++ b/dev/devicelab/pubspec.yaml
@@ -1,5 +1,4 @@
 name: flutter_devicelab
-version: 0.0.1
 author: Flutter Authors <flutter-dev@googlegroups.com>
 description: Flutter continuous integration performance and correctness tests.
 homepage: https://github.com/flutter/flutter
diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart
index dc50387..fabc95b 100644
--- a/dev/tools/dartdoc.dart
+++ b/dev/tools/dartdoc.dart
@@ -8,7 +8,6 @@
 
 import 'package:intl/intl.dart';
 import 'package:path/path.dart' as path;
-import 'update_versions.dart';
 
 /// Whether to report all error messages (true) or attempt to filter out some
 /// known false positives (false).
@@ -36,15 +35,18 @@
   if (path.basename(Directory.current.path) == 'tools')
     Directory.current = Directory.current.parent.parent;
 
-  final RawVersion version = new RawVersion('VERSION');
+  final ProcessResult flutter = Process.runSync('flutter', <String>[]);
+  final File versionFile = new File('version');
+  if (flutter.exitCode != 0 || !versionFile.existsSync())
+    throw new Exception('Failed to determine Flutter version.');
+  final String version = versionFile.readAsStringSync();
 
   // Create the pubspec.yaml file.
-  final StringBuffer buf = new StringBuffer('''
-name: Flutter
-homepage: https://flutter.io
-version: $version
-dependencies:
-''');
+  final StringBuffer buf = new StringBuffer();
+  buf.writeln('name: Flutter');
+  buf.writeln('homepage: https://flutter.io');
+  buf.writeln('version: $version');
+  buf.writeln('dependencies:');
   for (String package in findPackageNames()) {
     buf.writeln('  $package:');
     buf.writeln('    sdk: flutter');
@@ -100,7 +102,7 @@
     workingDirectory: 'dev/docs',
     environment: pubEnvironment,
   );
-  print('\n${result.stdout}');
+  print('\n${result.stdout}flutter version: $version\n');
 
   // Generate the documentation.
   final List<String> args = <String>[
@@ -159,7 +161,10 @@
   const int kGitRevisionLength = 10;
 
   final ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
-  String gitRevision = (gitResult.exitCode == 0) ? gitResult.stdout.trim() : 'unknown';
+  if (gitResult.exitCode != 0)
+    throw 'git exit with non-zero exit code: ${gitResult.exitCode}';
+  String gitRevision = gitResult.stdout.trim();
+
   gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
 
   final String timestamp = new DateFormat('yyyy-MM-dd HH:mm').format(new DateTime.now());
diff --git a/dev/tools/lib/roll_dev.dart b/dev/tools/lib/roll_dev.dart
new file mode 100644
index 0000000..1512363
--- /dev/null
+++ b/dev/tools/lib/roll_dev.dart
@@ -0,0 +1,158 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Rolls the dev channel.
+// Only tested on Linux.
+//
+// See: https://github.com/flutter/flutter/wiki/Release-process
+
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+
+const String kIncrement = 'increment';
+const String kX = 'x';
+const String kY = 'y';
+const String kZ = 'z';
+const String kHelp = 'help';
+
+void main(List<String> args) {
+  // If we're run from the `tools` dir, set the cwd to the repo root.
+  if (path.basename(Directory.current.path) == 'tools')
+    Directory.current = Directory.current.parent.parent;
+
+  final ArgParser argParser = new ArgParser(allowTrailingOptions: false);
+  argParser.addOption(
+    kIncrement,
+    help: 'Specifies which part of the x.y.z version number to increment. Required.',
+    valueHelp: 'level',
+    allowed: <String>[kX, kY, kZ],
+    allowedHelp: <String, String>{
+      kX: 'Indicates a major development, e.g. typically changed after a big press event.',
+      kY: 'Indicates a minor development, e.g. typically changed after a beta release.',
+      kZ: 'Indicates the least notable level of change. You normally want this.',
+    },
+  );
+  argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
+  ArgResults argResults;
+  try {
+    argResults = argParser.parse(args);
+  } on ArgParserException catch (error) {
+    print(error.message);
+    print(argParser.usage);
+    exit(1);
+  }
+
+  final String level = argResults[kIncrement];
+  final bool help = argResults[kHelp];
+
+  if (help || level == null) {
+    print('roll_dev.dart --increment=x • update the version tags and roll a new dev build.\n');
+    print(argParser.usage);
+    exit(0);
+  }
+
+  runGit('checkout master', 'switch to master branch');
+
+  if (getGitOutput('status --porcelain', 'check status of your local checkout') != '') {
+    print('Your git repository is not clean. Try running "git clean -fd". Warning, this ');
+    print('will delete files! Run with -n to find out which ones.');
+    exit(1);
+  }
+
+  String version = getFullTag();
+  final Match match = parseFullTag(version);
+  if (match == null) {
+    print('Could not determine the version for this build.');
+    if (version.isNotEmpty)
+      print('Git reported the latest version as "$version", which does not fit the expected pattern.');
+    exit(1);
+  }
+
+  final List<int> parts = match.groups(<int>[1, 2, 3]).map(int.parse).toList();
+
+  if (match.group(4) == '0') {
+    print('This commit has already been released, as version ${parts.join(".")}.');
+    exit(0);
+  }
+
+  switch (level) {
+    case kX:
+      parts[0] += 1;
+      parts[1] = 0;
+      parts[2] = 0;
+      break;
+    case kY:
+      parts[1] += 1;
+      parts[2] = 0;
+      break;
+    case kZ:
+      parts[2] += 1;
+      break;
+    default:
+      print('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
+      exit(1);
+  }
+  version = parts.join('.');
+
+  runGit('fetch upstream', 'fetch upstream');
+  runGit('reset upstream/master --hard', 'check out master branch');
+  runGit('tag $version', 'tag the commit with the version label');
+
+  print('Your tree is ready to publish Flutter $version to the "dev" channel.');
+  stdout.write('Are you? [yes/no] ');
+  if (stdin.readLineSync() != 'yes') {
+    runGit('tag -d $version', 'remove the tag you did not want to publish');
+    print('The dev roll has been aborted.');
+    exit(0);
+  }
+
+  runGit('push upstream $version', 'publish the version');
+  runGit('push upstream HEAD:dev', 'land the new version on the "dev" branch');
+  print('Flutter version $version has been rolled to the "dev" channel!');
+}
+
+String getFullTag() {
+  return getGitOutput(
+    'describe --match v*.*.* --exclude *-* --first-parent --long --tags',
+    'obtain last released version number',
+  );
+}
+
+Match parseFullTag(String version) {
+  final RegExp versionPattern = new RegExp('^v([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9]+)-g([a-f0-9]+)\$');
+  return versionPattern.matchAsPrefix(version);
+}
+
+String getGitOutput(String command, String explanation) {
+  final ProcessResult result = _runGit(command);
+  if (result.stderr.isEmpty && result.exitCode == 0)
+    return result.stdout.trim();
+  _reportGitFailureAndExit(result, explanation);
+  return null; // for the analyzer's sake
+}
+
+void runGit(String command, String explanation) {
+  final ProcessResult result = _runGit(command);
+  if (result.exitCode != 0)
+    _reportGitFailureAndExit(result, explanation);
+}
+
+ProcessResult _runGit(String command) {
+  return Process.runSync('git', command.split(' '));
+}
+
+void _reportGitFailureAndExit(ProcessResult result, String explanation) {
+  if (result.exitCode != 0) {
+    print('Failed to $explanation. Git exitted with error code ${result.exitCode}.');
+  } else {
+    print('Failed to $explanation.');
+  }
+  if (result.stdout.isNotEmpty)
+    print('stdout from git:\n${result.stdout}\n');
+  if (result.stderr.isNotEmpty)
+    print('stderr from git:\n${result.stderr}\n');
+  exit(1);
+}
diff --git a/dev/tools/update_versions.dart b/dev/tools/update_versions.dart
deleted file mode 100644
index 8f62bcc..0000000
--- a/dev/tools/update_versions.dart
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Updates the version numbers of the Flutter repo.
-// Only tested on Linux.
-//
-// See: https://github.com/flutter/flutter/wiki/Release-process
-
-import 'dart:io';
-
-import 'package:args/args.dart';
-import 'package:meta/meta.dart';
-import 'package:path/path.dart' as path;
-
-const String kIncrement = 'increment';
-const String kBrokeSdk = 'broke-sdk';
-const String kBrokeFramework = 'broke-framework';
-const String kBrokeTest = 'broke-test';
-const String kBrokeDriver = 'broke-driver';
-const String kMarkRelease = 'release';
-const String kHelp = 'help';
-
-const String kYamlVersionPrefix = 'version: ';
-const String kDev = '-dev';
-
-enum VersionKind { dev, release }
-
-void main(List<String> args) {
-  // If we're run from the `tools` dir, set the cwd to the repo root.
-  if (path.basename(Directory.current.path) == 'tools')
-    Directory.current = Directory.current.parent.parent;
-
-  final ArgParser argParser = new ArgParser();
-  argParser.addFlag(kIncrement, defaultsTo: false, help: 'Increment all the version numbers. Cannot be specified with --$kMarkRelease or with any --broke-* commands.');
-  argParser.addFlag(kBrokeSdk, defaultsTo: false, negatable: false, help: 'Increment the Flutter SDK version number to indicate that there has been a breaking change to the SDK (for example, to the command line options).');
-  argParser.addFlag(kBrokeFramework, defaultsTo: false, negatable: false, help: 'Increment the "flutter" package version number to indicate that there has been a breaking change to the Flutter framework.');
-  argParser.addFlag(kBrokeTest, defaultsTo: false, negatable: false, help: 'Increment the "flutter_test" package version number to indicate that there has been a breaking change to the test API framework.');
-  argParser.addFlag(kBrokeDriver, defaultsTo: false, negatable: false, help: 'Increment the "flutter_driver" package version number to indicate that there has been a breaking change to the driver API framework.');
-  argParser.addFlag(kMarkRelease, defaultsTo: false, help: 'Remove "-dev" from each version number. This is used when releasing. When not present, "-dev" is added to each version number. Cannot be specified with --$kIncrement or with any --broke-* commands.');
-  argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.');
-  final ArgResults argResults = argParser.parse(args);
-
-  final bool increment = argResults[kIncrement];
-  final bool brokeSdk = argResults[kBrokeSdk];
-  final bool brokeFramework = argResults[kBrokeFramework];
-  final bool brokeTest = argResults[kBrokeTest];
-  final bool brokeDriver = argResults[kBrokeDriver];
-  final bool brokeAnything = brokeSdk || brokeFramework || brokeTest || brokeDriver;
-  final VersionKind level = argResults[kMarkRelease] ? VersionKind.release : VersionKind.dev;
-  final bool help = argResults[kHelp];
-
-  if (help) {
-    print('update_versions.dart - update version numbers of Flutter packages and SDK');
-    print(argParser.usage);
-    exit(0);
-  }
-
-  final bool release = level == VersionKind.release;
-  if ((brokeAnything && release) || (brokeAnything && increment) || (release && increment)) {
-    print('You can either increment all the version numbers (--$kIncrement), indicate that some packages have had breaking changes (--broke-*), or switch to release mode (--$kMarkRelease).');
-    print('You cannot combine these, however.');
-    exit(1);
-  }
-
-  final RawVersion sdk = new RawVersion('VERSION');
-  final PubSpecVersion framework = new PubSpecVersion('packages/flutter/pubspec.yaml');
-  final PubSpecVersion test = new PubSpecVersion('packages/flutter_test/pubspec.yaml');
-  final PubSpecVersion driver = new PubSpecVersion('packages/flutter_driver/pubspec.yaml');
-
-  if (increment || brokeAnything)
-    sdk.increment(brokeAnything);
-  sdk.setMode(level);
-
-  if (increment || brokeFramework)
-    framework.increment(brokeFramework);
-  framework.setMode(level);
-
-  if (increment || brokeTest)
-    test.increment(brokeTest);
-  test.setMode(level);
-
-  if (increment || brokeDriver)
-    driver.increment(brokeDriver);
-  driver.setMode(level);
-
-  sdk.write();
-  framework.write();
-  test.write();
-  driver.write();
-
-  print('Flutter SDK is now at version: $sdk');
-  print('flutter package is now at version: $framework');
-  print('flutter_test package is now at version: $test');
-  print('flutter_driver package is now at version: $driver');
-
-  if (release) {
-    print('\nDuring the tagging step in the instructions, the commands will be:');
-    print('git tag $sdk');
-    print('git push upstream $sdk');
-  }
-}
-
-abstract class Version {
-  Version() {
-    read();
-  }
-
-  @protected
-  final List<int> version = <int>[];
-
-  @protected
-  VersionKind level;
-
-  @protected
-  bool dirty = false;
-
-  @protected
-  void read();
-
-  void interpret(String value) {
-    level = value.endsWith(kDev) ? VersionKind.dev : VersionKind.release;
-    if (level == VersionKind.dev)
-      value = value.substring(0, value.length - kDev.length);
-    version.addAll(value.split('.').map<int>(int.parse));
-  }
-
-  void increment(bool breaking) {
-    assert(version.length == 3);
-    if (breaking) {
-      version[1] += 1;
-      version[2] = 0;
-    } else {
-      version[2] += 1;
-    }
-    dirty = true;
-  }
-
-  void setMode(VersionKind value) {
-    if (value != level) {
-      level = value;
-      dirty = true;
-    }
-  }
-
-  void write();
-
-  @override
-  String toString() => version.join('.') + (level == VersionKind.dev ? kDev : '');
-}
-
-class PubSpecVersion extends Version {
-  PubSpecVersion(this.path);
-
-  final String path;
-
-  @override
-  void read() {
-    final List<String> lines = new File(path).readAsLinesSync();
-    final String versionLine = lines.where((String line) => line.startsWith(kYamlVersionPrefix)).single;
-    interpret(versionLine.substring(kYamlVersionPrefix.length));
-  }
-
-  @override
-  void write() {
-    if (!dirty)
-      return;
-    final List<String> lines = new File(path).readAsLinesSync();
-    for (int index = 0; index < lines.length; index += 1) {
-      final String line = lines[index];
-      if (line.startsWith(kYamlVersionPrefix)) {
-        lines[index] = '$kYamlVersionPrefix$this';
-        break;
-      }
-    }
-    new File(path).writeAsStringSync(lines.join('\n') + '\n');
-  }
-}
-
-class RawVersion extends Version {
-  RawVersion(this.path);
-
-  final String path;
-
-  @override
-  void read() {
-    final List<String> lines = new File(path).readAsLinesSync();
-    interpret(lines.where((String line) => line.isNotEmpty && !line.startsWith('#')).single);
-  }
-
-  @override
-  void write() {
-    if (!dirty)
-      return;
-    final List<String> lines = new File(path).readAsLinesSync();
-    for (int index = 0; index < lines.length; index += 1) {
-      final String line = lines[index];
-      if (line.isNotEmpty && !line.startsWith('#')) {
-        lines[index] = '$this';
-        break;
-      }
-    }
-    new File(path).writeAsStringSync(lines.join('\n') + '\n');
-  }
-}
diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml
index f71118e..57e8d98 100644
--- a/packages/flutter/pubspec.yaml
+++ b/packages/flutter/pubspec.yaml
@@ -1,5 +1,4 @@
 name: flutter
-version: 0.0.41-dev
 author: Flutter Authors <flutter-dev@googlegroups.com>
 description: A framework for writing Flutter applications
 homepage: http://flutter.io
diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml
index 7868691..4a94b49 100644
--- a/packages/flutter_driver/pubspec.yaml
+++ b/packages/flutter_driver/pubspec.yaml
@@ -1,5 +1,4 @@
 name: flutter_driver
-version: 0.0.19-dev
 description: Integration and performance test API for Flutter applications
 homepage: http://flutter.io
 author: Flutter Authors <flutter-dev@googlegroups.com>
diff --git a/packages/flutter_localizations/pubspec.yaml b/packages/flutter_localizations/pubspec.yaml
index aa1f228..298441a 100644
--- a/packages/flutter_localizations/pubspec.yaml
+++ b/packages/flutter_localizations/pubspec.yaml
@@ -1,5 +1,4 @@
 name: flutter_localizations
-version: 0.0.1-dev
 dependencies:
   # To update these, use "flutter update-packages --force-upgrade".
 
diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml
index 4367aa0..f42bfee 100644
--- a/packages/flutter_test/pubspec.yaml
+++ b/packages/flutter_test/pubspec.yaml
@@ -1,5 +1,4 @@
 name: flutter_test
-version: 0.0.19-dev
 dependencies:
   # To update these, use "flutter update-packages --force-upgrade".
 
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index d20863c..c3e3835 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -44,7 +44,7 @@
   await runner.run(args, <FlutterCommand>[
     new AnalyzeCommand(verboseHelp: verboseHelp),
     new BuildCommand(verboseHelp: verboseHelp),
-    new ChannelCommand(),
+    new ChannelCommand(verboseHelp: verboseHelp),
     new CleanCommand(),
     new InjectPluginsCommand(hidden: !verboseHelp),
     new ConfigCommand(verboseHelp: verboseHelp),
diff --git a/packages/flutter_tools/lib/src/commands/channel.dart b/packages/flutter_tools/lib/src/commands/channel.dart
index 9b1ec42..4c1367d 100644
--- a/packages/flutter_tools/lib/src/commands/channel.dart
+++ b/packages/flutter_tools/lib/src/commands/channel.dart
@@ -9,8 +9,19 @@
 import '../cache.dart';
 import '../globals.dart';
 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.',
+      defaultsTo: false,
+      hide: !verboseHelp,
+    );
+  }
+
   @override
   final String name = 'channel';
 
@@ -24,7 +35,7 @@
   Future<Null> runCommand() {
     switch (argResults.rest.length) {
       case 0:
-        return _listChannels();
+        return _listChannels(showAll: argResults['all']);
       case 1:
         return _switchChannel(argResults.rest[0]);
       default:
@@ -32,10 +43,12 @@
     }
   }
 
-  Future<Null> _listChannels() async {
-    final String currentBranch = runSync(
-        <String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
-        workingDirectory: Cache.flutterRoot);
+  Future<Null> _listChannels({ bool showAll }) async {
+    // Beware: currentBranch could contain PII. See getBranchName().
+    final String currentChannel = FlutterVersion.instance.channel;
+    final String currentBranch = FlutterVersion.instance.getBranchName();
+
+    showAll = showAll || currentChannel != currentBranch;
 
     printStatus('Flutter channels:');
     final int result = await runCommandAndStreamOutput(
@@ -46,24 +59,45 @@
         if (split.length < 2)
           return null;
         final String branchName = split[1];
-        if (branchName.startsWith('HEAD'))
-          return null;
         if (branchName == currentBranch)
           return '* $branchName';
-        return '  $branchName';
+        if (!branchName.startsWith('HEAD ') &&
+            (showAll || FlutterVersion.officialChannels.contains(branchName)))
+          return '  $branchName';
+        return null;
       },
     );
     if (result != 0)
       throwToolExit('List channels failed: $result', exitCode: result);
   }
 
-  Future<Null> _switchChannel(String branchName) async {
-    printStatus('Switching to flutter channel named $branchName');
+  Future<Null> _switchChannel(String branchName) {
+    printStatus("Switching to flutter channel '$branchName'...");
+    if (FlutterVersion.obsoleteBranches.containsKey(branchName)) {
+      final String alternative = FlutterVersion.obsoleteBranches[branchName];
+      printStatus("This channel is obsolete. Consider switching to the '$alternative' channel instead.");
+    } else if (!FlutterVersion.officialChannels.contains(branchName)) {
+      printStatus('This is not an official channel. For a list of available channels, try "flutter channel".');
+    }
+    return _checkout(branchName);
+  }
+
+  static Future<Null> upgradeChannel() async {
+    final String channel = FlutterVersion.instance.channel;
+    if (FlutterVersion.obsoleteBranches.containsKey(channel)) {
+      final String alternative = FlutterVersion.obsoleteBranches[channel];
+      printStatus("Transitioning from '$channel' to '$alternative'...");
+      return _checkout(alternative);
+    }
+  }
+
+  static Future<Null> _checkout(String branchName) async {
     final int result = await runCommandAndStreamOutput(
       <String>['git', 'checkout', branchName],
       workingDirectory: Cache.flutterRoot,
+      prefix: 'git: ',
     );
     if (result != 0)
-      throwToolExit('Switch channel failed: $result', exitCode: result);
+      throwToolExit('Switching channels failed with error code $result.', exitCode: result);
   }
 }
diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index 77ac728..bce432b 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -14,6 +14,7 @@
 import '../globals.dart';
 import '../runner/flutter_command.dart';
 import '../version.dart';
+import 'channel.dart';
 
 class UpgradeCommand extends FlutterCommand {
   @override
@@ -39,6 +40,8 @@
 
     printStatus('Upgrading Flutter from ${Cache.flutterRoot}...');
 
+    await ChannelCommand.upgradeChannel();
+
     int code = await runCommandAndStreamOutput(
       <String>['git', 'pull', '--ff-only'],
       workingDirectory: Cache.flutterRoot,
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index dfaabdc..8af7acc 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -198,7 +198,7 @@
 
     final FlutterVersion version = FlutterVersion.instance;
 
-    messages.add(new ValidationMessage('Flutter at ${Cache.flutterRoot}'));
+    messages.add(new ValidationMessage('Flutter version ${version.frameworkVersion} at ${Cache.flutterRoot}'));
     if (Cache.flutterRoot.contains(' '))
       messages.add(new ValidationMessage.error(
         'Flutter SDK install paths with spaces are not yet supported. (https://github.com/flutter/flutter/issues/6577)\n'
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index 0c7a455..8476617 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -247,6 +247,7 @@
       flutterUsage.suppressAnalytics = true;
 
     _checkFlutterCopy();
+    await FlutterVersion.instance.ensureVersionFile();
     await FlutterVersion.instance.checkFlutterVersionFreshness();
 
     if (globalResults.wasParsed('packages'))
diff --git a/packages/flutter_tools/lib/src/usage.dart b/packages/flutter_tools/lib/src/usage.dart
index abdb3d6..e1efafb 100644
--- a/packages/flutter_tools/lib/src/usage.dart
+++ b/packages/flutter_tools/lib/src/usage.dart
@@ -24,7 +24,7 @@
   /// used for testing.
   Usage({ String settingsName: 'flutter', String versionOverride, String configDirOverride}) {
     final FlutterVersion flutterVersion = FlutterVersion.instance;
-    final String version = versionOverride ?? flutterVersion.getVersionString(whitelistBranchName: true);
+    final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
     _analytics = new AnalyticsIO(_kFlutterUA, settingsName, version,
         // Analyzer doesn't recognize that [Directory] objects match up due to a
         // conditional import.
@@ -34,7 +34,7 @@
     // Report a more detailed OS version string than package:usage does by default.
     _analytics.setSessionValue('cd1', os.name);
     // Send the branch name as the "channel".
-    _analytics.setSessionValue('cd2', flutterVersion.getBranchName(whitelistBranchName: true));
+    _analytics.setSessionValue('cd2', flutterVersion.getBranchName(redactUnknownBranches: true));
     // Record the host as the application installer ID - the context that flutter_tools is running in.
     if (platform.environment.containsKey('FLUTTER_HOST')) {
       _analytics.setSessionValue('aiid', platform.environment['FLUTTER_HOST']);
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index d8e752e..2051932 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -10,20 +10,13 @@
 
 import 'base/common.dart';
 import 'base/context.dart';
+import 'base/file_system.dart';
 import 'base/io.dart';
 import 'base/process.dart';
 import 'base/process_manager.dart';
 import 'cache.dart';
 import 'globals.dart';
 
-final Set<String> kKnownBranchNames = new Set<String>.from(<String>[
-  'master',
-  'alpha',
-  'hackathon',
-  'codelab',
-  'beta'
-]);
-
 class FlutterVersion {
   @visibleForTesting
   FlutterVersion(this._clock) {
@@ -42,6 +35,7 @@
 
     _frameworkRevision = _runGit('git log -n 1 --pretty=format:%H');
     _frameworkAge = _runGit('git log -n 1 --pretty=format:%ar');
+    _frameworkVersion = GitTagVersion.determine().frameworkVersionFor(_frameworkRevision);
   }
 
   final Clock _clock;
@@ -49,11 +43,31 @@
   String _repositoryUrl;
   String get repositoryUrl => _repositoryUrl;
 
+  static Set<String> officialChannels = new Set<String>.from(<String>[
+    'master',
+    'dev',
+    'beta',
+    'release',
+  ]);
+
+  /// This maps old branch names to the names of branches that replaced them.
+  ///
+  /// For example, in early 2018 we changed from having an "alpha" branch to
+  /// having a "dev" branch, so anyone using "alpha" now gets transitioned to
+  /// "dev".
+  static Map<String, String> obsoleteBranches = <String, String>{
+    'alpha': 'dev',
+    'hackathon': 'dev',
+    'codelab': 'dev',
+  };
+
   String _channel;
-  /// `master`, `alpha`, `hackathon`, ...
+  /// The channel is the upstream branch.
+  /// `master`, `dev`, `beta`, `release`; or old ones, like `alpha`, `hackathon`, ...
   String get channel => _channel;
 
-  /// The name of the local branch
+  /// The name of the local branch.
+  /// Use getBranchName() to read this.
   String _branch;
 
   String _frameworkRevision;
@@ -63,6 +77,9 @@
   String _frameworkAge;
   String get frameworkAge => _frameworkAge;
 
+  String _frameworkVersion;
+  String get frameworkVersion => _frameworkVersion;
+
   String get frameworkDate => frameworkCommitDate;
 
   String get dartSdkVersion => Cache.instance.dartSdkVersion.split(' ')[0];
@@ -71,16 +88,19 @@
   String get engineRevision => Cache.instance.engineRevision;
   String get engineRevisionShort => _shortGitRevision(engineRevision);
 
-  String _runGit(String command) => runSync(command.split(' '), workingDirectory: Cache.flutterRoot);
+  Future<Null> ensureVersionFile() {
+    return fs.file(fs.path.join(Cache.flutterRoot, 'version')).writeAsString(_frameworkVersion);
+  }
 
   @override
   String toString() {
-    final String flutterText = 'Flutter • channel $channel • ${repositoryUrl == null ? 'unknown source' : repositoryUrl}';
+    final String versionText = frameworkVersion == 'unknown' ? '' : ' $frameworkVersion';
+    final String flutterText = 'Flutter$versionText • channel $channel • ${repositoryUrl == null ? 'unknown source' : repositoryUrl}';
     final String frameworkText = 'Framework • revision $frameworkRevisionShort ($frameworkAge) • $frameworkCommitDate';
     final String engineText = 'Engine • revision $engineRevisionShort';
     final String toolsText = 'Tools • Dart $dartSdkVersion';
 
-    // Flutter • channel master • https://github.com/flutter/flutter.git
+    // Flutter 1.3.922-pre.2 • channel master • https://github.com/flutter/flutter.git
     // Framework • revision 2259c59be8 • 19 minutes ago • 2016-08-15 22:51:40
     // Engine • revision fe509b0d96
     // Tools • Dart 1.19.0-dev.5.0
@@ -150,20 +170,22 @@
 
   static FlutterVersion get instance => context.putIfAbsent(FlutterVersion, () => new FlutterVersion(const Clock()));
 
-  /// Return a short string for the version (`alpha/a76bc8e22b`).
-  String getVersionString({bool whitelistBranchName: false}) {
-    return '${getBranchName(whitelistBranchName: whitelistBranchName)}/$frameworkRevisionShort';
+  /// Return a short string for the version (e.g. `master/0.0.59-pre.92`, `scroll_refactor/a76bc8e22b`).
+  String getVersionString({bool redactUnknownBranches: false}) {
+    if (frameworkVersion != 'unknown')
+      return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkVersion';
+    return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevisionShort';
   }
 
   /// Return the branch name.
   ///
-  /// If [whitelistBranchName] is true and the branch is unknown,
-  /// the branch name will be returned as 'dev'.
-  String getBranchName({ bool whitelistBranchName: false }) {
-    if (whitelistBranchName || _branch.isEmpty) {
+  /// If [redactUnknownBranches] is true and the branch is unknown,
+  /// the branch name will be returned as `'[user-branch]'`.
+  String getBranchName({ bool redactUnknownBranches: false }) {
+    if (redactUnknownBranches || _branch.isEmpty) {
       // Only return the branch names we know about; arbitrary branch names might contain PII.
-      if (!kKnownBranchNames.contains(_branch))
-        return 'dev';
+      if (!officialChannels.contains(_branch) && !obsoleteBranches.containsKey(_branch))
+        return '[user-branch]';
     }
     return _branch;
   }
@@ -424,6 +446,10 @@
   return '';
 }
 
+String _runGit(String command) {
+  return runSync(command.split(' '), workingDirectory: Cache.flutterRoot);
+}
+
 /// Runs [command] in the root of the Flutter installation and returns the
 /// standard output as a string.
 ///
@@ -445,3 +471,45 @@
     return '';
   return revision.length > 10 ? revision.substring(0, 10) : revision;
 }
+
+class GitTagVersion {
+  const GitTagVersion(this.x, this.y, this.z, this.commits, this.hash);
+  const GitTagVersion.unknown() : x = null, y = null, z = null, commits = 0, hash = '';
+
+  /// The X in vX.Y.Z.
+  final int x;
+
+  /// The Y in vX.Y.Z.
+  final int y;
+
+  /// The Z in vX.Y.Z.
+  final int z;
+
+  /// Number of commits since the vX.Y.Z tag.
+  final int commits;
+
+  /// The git hash (or an abbreviation thereof) for this commit.
+  final String hash;
+
+  static GitTagVersion determine() {
+    final String version = _runGit('git describe --match v*.*.* --exclude *-* --first-parent --long --tags');
+    final RegExp versionPattern = new RegExp('^v([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9]+)-g([a-f0-9]+)\$');
+    final List<String> parts = versionPattern.matchAsPrefix(version)?.groups(<int>[1, 2, 3, 4, 5]);
+    if (parts == null) {
+      printTrace('Could not interpret results of "git describe": $version');
+      return const GitTagVersion.unknown();
+    }
+    final List<int> parsedParts = parts.take(4).map<int>(
+      (String value) => int.parse(value, onError: (String value) => null),
+    ).toList();
+    return new GitTagVersion(parsedParts[0], parsedParts[1], parsedParts[2], parsedParts[3], parts[4]);
+  }
+
+  String frameworkVersionFor(String revision) {
+    if (x == null || y == null || z == null || !revision.startsWith(hash))
+      return 'unknown';
+    if (commits == 0)
+      return '$x.$y.$z';
+    return '$x.$y.${z + 1}-pre.$commits';
+  }
+}