Add "flutter downgrade" command (#50506)

diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 3b167e8..af10ac2 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -25,6 +25,7 @@
 import 'src/commands/daemon.dart';
 import 'src/commands/devices.dart';
 import 'src/commands/doctor.dart';
+import 'src/commands/downgrade.dart';
 import 'src/commands/drive.dart';
 import 'src/commands/emulators.dart';
 import 'src/commands/format.dart';
@@ -76,6 +77,7 @@
     DaemonCommand(hidden: !verboseHelp),
     DevicesCommand(),
     DoctorCommand(verbose: verbose),
+    DowngradeCommand(),
     DriveCommand(),
     EmulatorsCommand(),
     FormatCommand(),
diff --git a/packages/flutter_tools/lib/src/commands/downgrade.dart b/packages/flutter_tools/lib/src/commands/downgrade.dart
new file mode 100644
index 0000000..6be814f
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/downgrade.dart
@@ -0,0 +1,191 @@
+// 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 '../base/time.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({
+    PersistentToolState persistentToolState,
+    Logger logger,
+    ProcessManager processManager,
+    FlutterVersion flutterVersion,
+    AnsiTerminal terminal,
+    Stdio stdio,
+    FileSystem fileSystem,
+  }) : _terminal = terminal,
+       _flutterVersion = flutterVersion,
+       _persistentToolState = persistentToolState,
+       _processManager = processManager,
+       _stdio = stdio,
+       _logger = logger,
+       _fileSystem = fileSystem {
+    argParser.addOption(
+      'working-directory',
+      hide: true,
+      help: 'Override the downgrade working directory for integration testing.'
+    );
+    argParser.addFlag(
+      'prompt',
+      defaultsTo: true,
+      hide: true,
+      help: 'Disable the downgrade prompt for integration testing.'
+    );
+  }
+
+  AnsiTerminal _terminal;
+  FlutterVersion _flutterVersion;
+  PersistentToolState _persistentToolState;
+  ProcessUtils _processUtils;
+  ProcessManager _processManager;
+  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
+  Future<FlutterCommandResult> runCommand() async {
+    // Note: 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;
+    _logger ??= globals.logger;
+    _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 = stringArg('working-directory');
+      _flutterVersion = FlutterVersion(const SystemClock(), 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 String lastFlutterVesion = _persistentToolState.lastActiveVersion(channel);
+    final String currentFlutterVersion = _flutterVersion.frameworkRevision;
+    if (lastFlutterVesion == null || currentFlutterVersion == lastFlutterVesion) {
+      final String trailing = await _createErrorMessage(workingDirectory, channel);
+      throwToolExit(
+        'There is no previously recorded version for channel "$currentChannel".\n'
+        '$trailing'
+      );
+    }
+
+    // Detect unkown versions.
+    final RunResult parseResult = await _processUtils.run(<String>[
+      'git', 'describe', '--tags', lastFlutterVesion,
+    ], 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.
+    if (_stdio.hasTerminal && boolArg('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', lastFlutterVesion],
+        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();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index 705e4e6..d423513 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -10,6 +10,7 @@
 import '../base/io.dart';
 import '../base/os.dart';
 import '../base/process.dart';
+import '../base/time.dart';
 import '../cache.dart';
 import '../dart/pub.dart';
 import '../globals.dart' as globals;
@@ -34,6 +35,11 @@
         help: 'For the second half of the upgrade flow requiring the new '
               'version of Flutter. Should not be invoked manually, but '
               're-entrantly by the standard upgrade command.',
+      )
+      ..addOption(
+        'working-directory',
+        hide: true,
+        help: 'Override the upgrade working directoy for integration testing.'
       );
   }
 
@@ -50,36 +56,50 @@
 
   @override
   Future<FlutterCommandResult> runCommand() {
+    _commandRunner.workingDirectory = stringArg('working-directory') ?? Cache.flutterRoot;
     return _commandRunner.runCommand(
-      boolArg('force'),
-      boolArg('continue'),
-      GitTagVersion.determine(),
-      globals.flutterVersion,
+      force: boolArg('force'),
+      continueFlow: boolArg('continue'),
+      testFlow: stringArg('working-directory') != null,
+      gitTagVersion: GitTagVersion.determine(processUtils),
+      flutterVersion: stringArg('working-directory') == null
+        ? globals.flutterVersion
+        : FlutterVersion(const SystemClock(), _commandRunner.workingDirectory),
     );
   }
 }
 
 @visibleForTesting
 class UpgradeCommandRunner {
-  Future<FlutterCommandResult> runCommand(
-    bool force,
-    bool continueFlow,
-    GitTagVersion gitTagVersion,
-    FlutterVersion flutterVersion,
-  ) async {
+
+  String workingDirectory;
+
+  Future<FlutterCommandResult> runCommand({
+    @required bool force,
+    @required bool continueFlow,
+    @required bool testFlow,
+    @required GitTagVersion gitTagVersion,
+    @required FlutterVersion flutterVersion,
+  }) async {
     if (!continueFlow) {
-      await runCommandFirstHalf(force, gitTagVersion, flutterVersion);
+      await runCommandFirstHalf(
+        force: force,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
+        testFlow: testFlow,
+      );
     } else {
       await runCommandSecondHalf(flutterVersion);
     }
     return FlutterCommandResult.success();
   }
 
-  Future<void> runCommandFirstHalf(
-    bool force,
-    GitTagVersion gitTagVersion,
-    FlutterVersion flutterVersion,
-  ) async {
+  Future<void> runCommandFirstHalf({
+    @required bool force,
+    @required GitTagVersion gitTagVersion,
+    @required FlutterVersion flutterVersion,
+    @required bool testFlow,
+  }) async {
     await verifyUpstreamConfigured();
     if (!force && gitTagVersion == const GitTagVersion.unknown()) {
       // If the commit is a recognized branch and not master,
@@ -110,6 +130,7 @@
         'command with --force.'
       );
     }
+    recordState(flutterVersion);
     await resetChanges(gitTagVersion);
     await upgradeChannel(flutterVersion);
     final bool alreadyUpToDate = await attemptFastForward(flutterVersion);
@@ -117,11 +138,19 @@
       // If the upgrade was a no op, then do not continue with the second half.
       globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
       globals.printStatus('$flutterVersion');
-    } else {
+    } else if (!testFlow) {
       await flutterUpgradeContinue();
     }
   }
 
+  void recordState(FlutterVersion flutterVersion) {
+    final Channel channel = getChannelForName(flutterVersion.channel);
+    if (channel == null) {
+      return;
+    }
+    globals.persistentToolState.updateLastActiveVersion(flutterVersion.frameworkRevision, channel);
+  }
+
   Future<void> flutterUpgradeContinue() async {
     final int code = await processUtils.stream(
       <String>[
@@ -130,7 +159,7 @@
         '--continue',
         '--no-version-check',
       ],
-      workingDirectory: Cache.flutterRoot,
+      workingDirectory: workingDirectory,
       allowReentrantFlutter: true,
       environment: Map<String, String>.of(globals.platform.environment),
     );
@@ -156,7 +185,7 @@
       final RunResult result = await processUtils.run(
         <String>['git', 'status', '-s'],
         throwOnError: true,
-        workingDirectory: Cache.flutterRoot,
+        workingDirectory: workingDirectory,
       );
       return result.stdout.trim().isNotEmpty;
     } on ProcessException catch (error) {
@@ -179,13 +208,13 @@
       await processUtils.run(
         <String>[ 'git', 'rev-parse', '@{u}'],
         throwOnError: true,
-        workingDirectory: Cache.flutterRoot,
+        workingDirectory: workingDirectory,
       );
     } catch (e) {
       throwToolExit(
         'Unable to upgrade Flutter: no origin repository configured. '
         "Run 'git remote add origin "
-        "https://github.com/flutter/flutter' in ${Cache.flutterRoot}",
+        "https://github.com/flutter/flutter' in $workingDirectory",
       );
     }
   }
@@ -206,7 +235,7 @@
       await processUtils.run(
         <String>['git', 'reset', '--hard', tag],
         throwOnError: true,
-        workingDirectory: Cache.flutterRoot,
+        workingDirectory: workingDirectory,
       );
     } on ProcessException catch (error) {
       throwToolExit(
@@ -223,7 +252,7 @@
   /// If the user is on a deprecated channel, attempts to migrate them off of
   /// it.
   Future<void> upgradeChannel(FlutterVersion flutterVersion) async {
-    globals.printStatus('Upgrading Flutter from ${Cache.flutterRoot}...');
+    globals.printStatus('Upgrading Flutter from $workingDirectory...');
     await ChannelCommand.upgradeChannel();
   }
 
@@ -237,7 +266,7 @@
   Future<bool> attemptFastForward(FlutterVersion oldFlutterVersion) async {
     final int code = await processUtils.stream(
       <String>['git', 'pull', '--ff'],
-      workingDirectory: Cache.flutterRoot,
+      workingDirectory: workingDirectory,
       mapFunction: (String line) => matchesGitLine(line) ? null : line,
     );
     if (code != 0) {
@@ -247,7 +276,7 @@
     // Check if the upgrade did anything.
     bool alreadyUpToDate = false;
     try {
-      final FlutterVersion newFlutterVersion = FlutterVersion();
+      final FlutterVersion newFlutterVersion = FlutterVersion(const SystemClock(), workingDirectory);
       alreadyUpToDate = newFlutterVersion.channel == oldFlutterVersion.channel &&
         newFlutterVersion.frameworkRevision == oldFlutterVersion.frameworkRevision;
     } catch (e) {
@@ -268,7 +297,7 @@
       <String>[
         globals.fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache',
       ],
-      workingDirectory: Cache.flutterRoot,
+      workingDirectory: workingDirectory,
       allowReentrantFlutter: true,
       environment: Map<String, String>.of(globals.platform.environment),
     );
@@ -296,7 +325,7 @@
       <String>[
         globals.fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor',
       ],
-      workingDirectory: Cache.flutterRoot,
+      workingDirectory: workingDirectory,
       allowReentrantFlutter: true,
     );
   }
diff --git a/packages/flutter_tools/lib/src/commands/version.dart b/packages/flutter_tools/lib/src/commands/version.dart
index cb8f6cc..b70bba5 100644
--- a/packages/flutter_tools/lib/src/commands/version.dart
+++ b/packages/flutter_tools/lib/src/commands/version.dart
@@ -57,8 +57,29 @@
     final List<String> tags = await getTags();
     if (argResults.rest.isEmpty) {
       tags.forEach(globals.printStatus);
-      return const FlutterCommandResult(ExitStatus.success);
+      return FlutterCommandResult.success();
     }
+
+    globals.printStatus(
+      '╔══════════════════════════════════════════════════════════════════════════════╗\n'
+      '║ Warning: "flutter version" will leave the SDK in a detached HEAD state.      ║\n'
+      '║ If you are using the command to return to a previously installed SDK version ║\n'
+      '║ consider using the "flutter downgrade" command instead.                      ║\n'
+      '╚══════════════════════════════════════════════════════════════════════════════╝\n',
+      emphasis: true,
+    );
+    if (globals.stdio.stdinHasTerminal) {
+      globals.terminal.usesTerminalUi = true;
+      final String result = await globals.terminal.promptForCharInput(
+        <String>['y', 'n'],
+        logger: globals.logger,
+        prompt: 'Are you sure you want to proceed?'
+      );
+      if (result == 'n') {
+        return FlutterCommandResult.success();
+      }
+    }
+
     final String version = argResults.rest[0].replaceFirst('v', '');
     if (!tags.contains('v$version')) {
       globals.printError('There is no version: $version');
diff --git a/packages/flutter_tools/lib/src/persistent_tool_state.dart b/packages/flutter_tools/lib/src/persistent_tool_state.dart
index 8d6100c..49602ae 100644
--- a/packages/flutter_tools/lib/src/persistent_tool_state.dart
+++ b/packages/flutter_tools/lib/src/persistent_tool_state.dart
@@ -9,6 +9,7 @@
 import 'base/context.dart';
 import 'base/file_system.dart';
 import 'base/logger.dart';
+import 'version.dart';
 
 /// A class that represents global (non-project-specific) internal state that
 /// must persist across tool invocations.
@@ -37,6 +38,14 @@
   ///
   /// May give null if the value has not been set.
   bool redisplayWelcomeMessage;
+
+  /// Returns the last active version for a given [channel].
+  ///
+  /// If there was no active prior version, returns `null` instead.
+  String lastActiveVersion(Channel channel);
+
+  /// Update the last active version for a given [channel].
+  void updateLastActiveVersion(String fullGitHash, Channel channel);
 }
 
 class _DefaultPersistentToolState implements PersistentToolState {
@@ -63,6 +72,12 @@
 
   static const String _kFileName = '.flutter_tool_state';
   static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message';
+  static const Map<Channel, String> _lastActiveVersionKeys = <Channel,String>{
+    Channel.master: 'last-active-master-version',
+    Channel.dev: 'last-active-dev-version',
+    Channel.beta: 'last-active-beta-version',
+    Channel.stable: 'last-active-stable-version'
+  };
 
   final Config _config;
 
@@ -72,7 +87,25 @@
   }
 
   @override
+  String lastActiveVersion(Channel channel) {
+    final String versionKey = _versionKeyFor(channel);
+    assert(versionKey != null);
+    return _config.getValue(versionKey) as String;
+  }
+
+  @override
   set redisplayWelcomeMessage(bool value) {
     _config.setValue(_kRedisplayWelcomeMessage, value);
   }
+
+  @override
+  void updateLastActiveVersion(String fullGitHash, Channel channel) {
+    final String versionKey = _versionKeyFor(channel);
+    assert(versionKey != null);
+    _config.setValue(versionKey, fullGitHash);
+  }
+
+  String _versionKeyFor(Channel channel) {
+    return _lastActiveVersionKeys[channel];
+  }
 }
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index e5cf0c1..8a87986 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -15,14 +15,45 @@
 import 'convert.dart';
 import 'globals.dart' as globals;
 
+/// The names of each channel/branch in order of increasing stability.
+enum Channel {
+  master,
+  dev,
+  beta,
+  stable,
+}
+
+/// Retrieve a human-readable name for a given [channel].
+///
+/// Requires [FlutterVersion.officialChannels] to be correctly ordered.
+String getNameForChannel(Channel channel) {
+  return FlutterVersion.officialChannels.elementAt(channel.index);
+}
+
+/// Retrieve the [Channel] representation for a string [name].
+///
+/// Returns `null` if [name] is not in the list of official channels, according
+/// to [FlutterVersion.officialChannels].
+Channel getChannelForName(String name) {
+  if (FlutterVersion.officialChannels.contains(name)) {
+    return Channel.values[FlutterVersion.officialChannels.toList().indexOf(name)];
+  }
+  return null;
+}
+
 class FlutterVersion {
-  FlutterVersion([this._clock = const SystemClock()]) {
-    _frameworkRevision = _runGit(gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '));
-    _gitTagVersion = GitTagVersion.determine();
+  FlutterVersion([this._clock = const SystemClock(), this._workingDirectory]) {
+    _frameworkRevision = _runGit(
+      gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '),
+      processUtils,
+      _workingDirectory,
+    );
+    _gitTagVersion = GitTagVersion.determine(processUtils, _workingDirectory);
     _frameworkVersion = gitTagVersion.frameworkVersionFor(_frameworkRevision);
   }
 
   final SystemClock _clock;
+  final String _workingDirectory;
 
   String _repositoryUrl;
   String get repositoryUrl {
@@ -60,11 +91,19 @@
   /// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ...
   String get channel {
     if (_channel == null) {
-      final String channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}');
+      final String channel = _runGit(
+        'git rev-parse --abbrev-ref --symbolic @{u}',
+        processUtils,
+        _workingDirectory,
+      );
       final int slash = channel.indexOf('/');
       if (slash != -1) {
         final String remote = channel.substring(0, slash);
-        _repositoryUrl = _runGit('git ls-remote --get-url $remote');
+        _repositoryUrl = _runGit(
+          'git ls-remote --get-url $remote',
+          processUtils,
+          _workingDirectory,
+        );
         _channel = channel.substring(slash + 1);
       } else if (channel.isEmpty) {
         _channel = 'unknown';
@@ -88,7 +127,11 @@
 
   String _frameworkAge;
   String get frameworkAge {
-    return _frameworkAge ??= _runGit(gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '));
+    return _frameworkAge ??= _runGit(
+      gitLog(<String>['-n', '1', '--pretty=format:%ar']).join(' '),
+      processUtils,
+      _workingDirectory,
+    );
   }
 
   String _frameworkVersion;
@@ -226,7 +269,7 @@
   /// the branch name will be returned as `'[user-branch]'`.
   String getBranchName({ bool redactUnknownBranches = false }) {
     _branch ??= () {
-      final String branch = _runGit('git rev-parse --abbrev-ref HEAD');
+      final String branch = _runGit('git rev-parse --abbrev-ref HEAD', processUtils);
       return branch == 'HEAD' ? channel : branch;
     }();
     if (redactUnknownBranches || _branch.isEmpty) {
@@ -599,10 +642,10 @@
   return '';
 }
 
-String _runGit(String command) {
+String _runGit(String command, ProcessUtils processUtils, [String workingDirectory]) {
   return processUtils.runSync(
     command.split(' '),
-    workingDirectory: Cache.flutterRoot,
+    workingDirectory: workingDirectory ?? Cache.flutterRoot,
   ).stdout.trim();
 }
 
@@ -658,8 +701,8 @@
   /// The git hash (or an abbreviation thereof) for this commit.
   final String hash;
 
-  static GitTagVersion determine() {
-    return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags'));
+  static GitTagVersion determine(ProcessUtils processUtils, [String workingDirectory]) {
+    return parse(_runGit('git describe --match v*.*.* --first-parent --long --tags', processUtils, workingDirectory));
   }
 
   static GitTagVersion parse(String version) {
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
new file mode 100644
index 0000000..db627c2
--- /dev/null
+++ b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart
@@ -0,0 +1,249 @@
+// 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:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/downgrade.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  FileSystem fileSystem;
+  BufferLogger bufferLogger;
+  AnsiTerminal terminal;
+  ProcessManager processManager;
+  MockStdio mockStdio;
+  FlutterVersion flutterVersion;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  tearDownAll(() {
+    Cache.enableLocking();
+  });
+
+  setUp(() {
+    flutterVersion = MockFlutterVersion();
+    mockStdio = MockStdio();
+    processManager = FakeProcessManager.any();
+    terminal = MockTerminal();
+    fileSystem = MemoryFileSystem.test();
+    bufferLogger = BufferLogger(terminal: terminal, outputPreferences: OutputPreferences.test());
+  });
+
+  testUsingContext('Downgrade exits on unknown channel', () async {
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"invalid"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message: 'Flutter is not currently on a known channel.'));
+  });
+
+  testUsingContext('Downgrade exits on no recorded version', () async {
+    when(flutterVersion.channel).thenReturn('dev');
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"abcd"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'abcd'
+          ],
+          exitCode: 0,
+          stdout: 'v1.2.3'
+        )
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message:
+        'There is no previously recorded version for channel "dev".\n'
+        'Channel "master" was previously on: v1.2.3.'
+      ),
+    );
+  });
+
+  testUsingContext('Downgrade exits on unknown recorded version', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"invalid"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'invalid'
+          ],
+          exitCode: 1,
+        )
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
+      throwsToolExit(message: 'Failed to parse version for downgrade'));
+  });
+
+   testUsingContext('Downgrade prompts for user input when terminal is attached - y', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(true);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    when(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).thenAnswer((Invocation invocation) async => 'y');
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verify(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).called(1);
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+
+   testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(true);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    when(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).thenAnswer((Invocation invocation) async => 'n');
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verify(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    )).called(1);
+    expect(bufferLogger.statusText, isNot(contains('Success')));
+  });
+
+  testUsingContext('Downgrade does not prompt when there is no terminal', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(false);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(
+        directory: fileSystem.currentDirectory,
+        logger: bufferLogger,
+      ),
+      processManager: processManager,
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    verifyNever(terminal.promptForCharInput(
+      const <String>['y', 'n'],
+      prompt: anyNamed('prompt'),
+      logger: anyNamed('logger'),
+    ));
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+
+  testUsingContext('Downgrade performs correct git commands', () async {
+    when(flutterVersion.channel).thenReturn('master');
+    when(mockStdio.hasTerminal).thenReturn(false);
+    fileSystem.currentDirectory.childFile('.flutter_tool_state')
+      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
+    final DowngradeCommand command = DowngradeCommand(
+      persistentToolState: PersistentToolState.test(
+        directory: fileSystem.currentDirectory,
+        logger: bufferLogger,
+      ),
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'git', 'describe', '--tags', 'g6b00b5e88'
+          ],
+          stdout: 'v1.2.3',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'git', 'reset', '--hard', 'g6b00b5e88'
+          ],
+        ),
+        const FakeCommand(
+          command: <String>[
+            'git', 'checkout', 'master', '--'
+          ]
+        ),
+      ]),
+      terminal: terminal,
+      stdio: mockStdio,
+      flutterVersion: flutterVersion,
+      logger: bufferLogger,
+    );
+    applyMocksToCommand(command);
+
+    await createTestCommandRunner(command).run(const <String>['downgrade']);
+
+    expect(bufferLogger.statusText, contains('Success'));
+  });
+}
+
+class MockTerminal extends Mock implements AnsiTerminal {}
+class MockStdio extends Mock implements Stdio {}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
index 98922e0..74596c3 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
@@ -8,11 +8,13 @@
 
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/version.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
 
 import '../../src/common.dart';
 import '../../src/context.dart';
@@ -20,10 +22,18 @@
 
 void main() {
   group('version', () {
+    MockStdio mockStdio;
+
     setUpAll(() {
       Cache.disableLocking();
     });
 
+    setUp(() {
+      mockStdio = MockStdio();
+      when(mockStdio.stdinHasTerminal).thenReturn(false);
+      when(mockStdio.hasTerminal).thenReturn(false);
+    });
+
     testUsingContext('version ls', () async {
       final VersionCommand command = VersionCommand();
       await createTestCommandRunner(command).run(<String>[
@@ -33,11 +43,18 @@
       expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
-    testUsingContext('version switch', () async {
+    testUsingContext('version switch prompt is accepted', () async {
+      when(mockStdio.stdinHasTerminal).thenReturn(true);
       const String version = '10.0.0';
       final VersionCommand command = VersionCommand();
+      when(globals.terminal.promptForCharInput(<String>['y', 'n'],
+        logger: anyNamed('logger'),
+        prompt: 'Are you sure you want to proceed?')
+      ).thenAnswer((Invocation invocation) async => 'y');
+
       await createTestCommandRunner(command).run(<String>[
         'version',
         '--no-pub',
@@ -46,6 +63,29 @@
       expect(testLogger.statusText, contains('Switching Flutter to version $version'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
+      AnsiTerminal: () => MockTerminal(),
+    });
+
+    testUsingContext('version switch prompt is declined', () async {
+      when(mockStdio.stdinHasTerminal).thenReturn(true);
+      const String version = '10.0.0';
+      final VersionCommand command = VersionCommand();
+      when(globals.terminal.promptForCharInput(<String>['y', 'n'],
+        logger: anyNamed('logger'),
+        prompt: 'Are you sure you want to proceed?')
+      ).thenAnswer((Invocation invocation) async => 'n');
+
+      await createTestCommandRunner(command).run(<String>[
+        'version',
+        '--no-pub',
+        version,
+      ]);
+      expect(testLogger.statusText, isNot(contains('Switching Flutter to version $version')));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
+      AnsiTerminal: () => MockTerminal(),
     });
 
     testUsingContext('version switch, latest commit query fails', () async {
@@ -59,6 +99,7 @@
       expect(testLogger.errorText, contains('git failed'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(latestCommitFails: true),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('latest commit is parsable when query fails', () {
@@ -69,6 +110,7 @@
       );
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(latestCommitFails: true),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('switch to not supported version without force', () async {
@@ -82,6 +124,7 @@
       expect(testLogger.errorText, contains('Version command is not supported in'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('switch to not supported version with force', () async {
@@ -96,6 +139,7 @@
       expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext('tool exit on confusing version', () async {
@@ -111,6 +155,7 @@
       );
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(),
+      Stdio: () => mockStdio,
     });
 
     testUsingContext("exit tool if can't get the tags", () async {
@@ -123,10 +168,13 @@
       }
     }, overrides: <Type, Generator>{
       ProcessManager: () => MockProcessManager(failGitTag: true),
+      Stdio: () => mockStdio,
     });
   });
 }
 
+class MockTerminal extends Mock implements AnsiTerminal {}
+class MockStdio extends Mock implements Stdio {}
 class MockProcessManager extends Mock implements ProcessManager {
   MockProcessManager({
     this.failGitTag = false,
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 1d71094..f5ac6fb 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -57,10 +57,11 @@
 
     testUsingContext('throws on unknown tag, official branch,  noforce', () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        const GitTagVersion.unknown(),
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: const GitTagVersion.unknown(),
+        flutterVersion: flutterVersion,
       );
       expect(result, throwsToolExit());
     }, overrides: <Type, Generator>{
@@ -69,10 +70,11 @@
 
     testUsingContext('does not throw on unknown tag, official branch, force', () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        true,
-        false,
-        const GitTagVersion.unknown(),
-        flutterVersion,
+        force: true,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: const GitTagVersion.unknown(),
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -83,10 +85,11 @@
     testUsingContext('throws tool exit with uncommitted changes', () async {
       fakeCommandRunner.willHaveUncomittedChanges = true;
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(result, throwsToolExit());
     }, overrides: <Type, Generator>{
@@ -97,10 +100,11 @@
       fakeCommandRunner.willHaveUncomittedChanges = true;
 
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        true,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: true,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -110,10 +114,11 @@
 
     testUsingContext("Doesn't throw on known tag, dev branch, no force", () async {
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
     }, overrides: <Type, Generator>{
@@ -124,10 +129,11 @@
     testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async {
       fakeCommandRunner.alreadyUpToDate = true;
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
-        false,
-        false,
-        gitTagVersion,
-        flutterVersion,
+        force: false,
+        continueFlow: false,
+        testFlow: false,
+        gitTagVersion: gitTagVersion,
+        flutterVersion: flutterVersion,
       );
       expect(await result, FlutterCommandResult.success());
       verifyNever(globals.processManager.start(
diff --git a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
index 59dead0..1f4bfcf 100644
--- a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
+++ b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
@@ -6,6 +6,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/persistent_tool_state.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
@@ -14,8 +15,8 @@
 
 void main() {
   testWithoutContext('state can be set and persists', () {
-    final MemoryFileSystem fs = MemoryFileSystem();
-    final Directory directory = fs.directory('state_dir');
+    final MemoryFileSystem fileSystem = MemoryFileSystem();
+    final Directory directory = fileSystem.directory('state_dir');
     directory.createSync();
     final File stateFile = directory.childFile('.flutter_tool_state');
     final PersistentToolState state1 = PersistentToolState.test(
@@ -35,4 +36,28 @@
     );
     expect(state2.redisplayWelcomeMessage, false);
   });
+
+  testWithoutContext('channel versions can be cached and stored', () {
+    final MemoryFileSystem fileSystem = MemoryFileSystem();
+    final Directory directory = fileSystem.directory('state_dir')..createSync();
+    final PersistentToolState state1 = PersistentToolState.test(
+      directory: directory,
+      logger: MockLogger(),
+    );
+
+    state1.updateLastActiveVersion('abc', Channel.master);
+    state1.updateLastActiveVersion('def', Channel.dev);
+    state1.updateLastActiveVersion('ghi', Channel.beta);
+    state1.updateLastActiveVersion('jkl', Channel.stable);
+
+    final PersistentToolState state2 = PersistentToolState.test(
+      directory: directory,
+      logger: MockLogger(),
+    );
+
+    expect(state2.lastActiveVersion(Channel.master), 'abc');
+    expect(state2.lastActiveVersion(Channel.dev), 'def');
+    expect(state2.lastActiveVersion(Channel.beta), 'ghi');
+    expect(state2.lastActiveVersion(Channel.stable), 'jkl');
+  });
 }
diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
new file mode 100644
index 0000000..6c1bc2c
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
@@ -0,0 +1,122 @@
+// 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:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/process.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+
+const String _kInitialVersion = 'v1.9.1+hotfix.6';
+const String _kBranch = 'stable';
+const FileSystem fileSystem = LocalFileSystem();
+const ProcessManager processManager = LocalProcessManager();
+final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
+  terminal: AnsiTerminal(
+    platform: const LocalPlatform(),
+    stdio: const Stdio(),
+  ),
+  stdio: const Stdio(),
+  outputPreferences: OutputPreferences.test(wrapText: true),
+  timeoutConfiguration: const TimeoutConfiguration(),
+));
+final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+
+/// A test for flutter upgrade & downgrade that checks out a parallel flutter repo.
+void main() {
+  Directory parentDirectory;
+
+  setUp(() {
+    parentDirectory = fileSystem.systemTempDirectory
+      .createTempSync('flutter_tools.');
+    parentDirectory.createSync(recursive: true);
+  });
+
+  tearDown(() {
+    try {
+      parentDirectory.deleteSync(recursive: true);
+    } on FileSystemException {
+      print('Failed to delete test directory');
+    }
+  });
+
+  test('Can upgrade and downgrade a Flutter checkout', () async {
+    final Directory testDirectory = parentDirectory.childDirectory('flutter');
+    testDirectory.createSync(recursive: true);
+
+    // Enable longpaths for windows integration test.
+    await processManager.run(<String>[
+      'git', 'config', '--system', 'core.longpaths', 'true',
+    ]);
+
+    // Step 1. Clone the dev branch of flutter into the test directory.
+    await processUtils.stream(<String>[
+      'git',
+      'clone',
+      'https://github.com/flutter/flutter.git',
+    ], workingDirectory: parentDirectory.path, trace: true);
+
+    // Step 2. Switch to the dev branch.
+    await processUtils.stream(<String>[
+      'git',
+      'checkout',
+      '--track',
+      '-b',
+      _kBranch,
+      'origin/$_kBranch',
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 3. Revert to a prior version.
+    await processUtils.stream(<String>[
+      'git',
+      'reset',
+      '--hard',
+      _kInitialVersion,
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 4. Upgrade to the newest dev. This should update the persistent
+    // tool state with the sha for v1.14.3
+    await processUtils.stream(<String>[
+      flutterBin,
+      'upgrade',
+      '--working-directory=${testDirectory.path}'
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 5. Verify that the version is different.
+    final RunResult versionResult = await processUtils.run(<String>[
+      'git',
+      'describe',
+      '--match',
+      'v*.*.*',
+      '--first-parent',
+      '--long',
+      '--tags',
+    ], workingDirectory: testDirectory.path);
+    expect(versionResult.stdout, isNot(contains(_kInitialVersion)));
+
+    // Step 6. Downgrade back to initial version.
+    await processUtils.stream(<String>[
+       flutterBin,
+      'downgrade',
+      '--no-prompt',
+      '--working-directory=${testDirectory.path}'
+    ], workingDirectory: testDirectory.path, trace: true);
+
+    // Step 7. Verify downgraded version matches original version.
+    final RunResult oldVersionResult = await processUtils.run(<String>[
+      'git',
+      'describe',
+      '--match',
+      'v*.*.*',
+      '--first-parent',
+      '--long',
+      '--tags',
+    ], workingDirectory: testDirectory.path);
+    expect(oldVersionResult.stdout, contains(_kInitialVersion));
+  });
+}