Support using flutter with specific version (#26840)

* Support using flutter with specific version

* Set min supported version to 1.2.1
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index bf95f0c..160cfc3 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -33,6 +33,7 @@
 import 'src/commands/trace.dart';
 import 'src/commands/update_packages.dart';
 import 'src/commands/upgrade.dart';
+import 'src/commands/version.dart';
 import 'src/runner/flutter_command.dart';
 
 /// Main entry point for commands.
@@ -77,6 +78,7 @@
     TraceCommand(),
     UpdatePackagesCommand(hidden: !verboseHelp),
     UpgradeCommand(),
+    VersionCommand(),
   ], verbose: verbose,
      muteCommandLogging: muteCommandLogging,
      verboseHelp: verboseHelp);
diff --git a/packages/flutter_tools/lib/src/commands/version.dart b/packages/flutter_tools/lib/src/commands/version.dart
new file mode 100644
index 0000000..18b1f43
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/version.dart
@@ -0,0 +1,132 @@
+// Copyright 2019 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.
+
+import 'dart:async';
+
+import '../base/common.dart';
+import '../base/file_system.dart';
+import '../base/os.dart';
+import '../base/process.dart';
+import '../base/version.dart';
+import '../cache.dart';
+import '../dart/pub.dart';
+import '../globals.dart';
+import '../runner/flutter_command.dart';
+import '../version.dart';
+
+class VersionCommand extends FlutterCommand {
+  VersionCommand(): super() {
+    argParser.addFlag('force',
+      abbr: 'f',
+      help: 'Force switch to older Flutter versions that do not include a version command',
+    );
+  }
+
+  @override
+  final String name = 'version';
+
+  @override
+  final String description = 'List or switch flutter versions.';
+
+  // The first version of Flutter which includes the flutter version command. Switching to older
+  // versions will require the user to manually upgrade.
+  Version minSupportedVersion = Version.parse('1.2.1');
+
+  Future<List<String>> getTags() async {
+    final RunResult runResult = await runCheckedAsync(
+      <String>['git', 'tag', '-l', 'v*'],
+      workingDirectory: Cache.flutterRoot
+    );
+    return runResult.toString().split('\n');
+  }
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    final List<String> tags = await getTags();
+    if (argResults.rest.isEmpty) {
+      tags.forEach(printStatus);
+      return const FlutterCommandResult(ExitStatus.success);
+    }
+    final String version = argResults.rest[0].replaceFirst('v', '');
+    if (!tags.contains('v$version')) {
+      printError('There is no version: $version');
+    }
+
+    // check min supported version
+    final Version targetVersion = Version.parse(version);
+    bool withForce = false;
+    if (targetVersion < minSupportedVersion) {
+      if (!argResults['force']) {
+        printError(
+          'Version command is not supported in $targetVersion and it is supported since version $minSupportedVersion'
+          'which means if you switch to version $minSupportedVersion then you can not use version command.'
+          'If you really want to switch to version $targetVersion, please use `--force` flag: `flutter version --force $targetVersion`.'
+        );
+        return const FlutterCommandResult(ExitStatus.success);
+      }
+      withForce = true;
+    }
+
+    try {
+      await runCheckedAsync(
+        <String>['git', 'checkout', 'v$version'],
+        workingDirectory: Cache.flutterRoot
+      );
+    } catch (e) {
+      throwToolExit('Unable to checkout version branch for version $version.');
+    }
+
+    final FlutterVersion flutterVersion = FlutterVersion();
+
+    printStatus('Switching Flutter to version ${flutterVersion.frameworkVersion}${withForce ? ' with force' : ''}');
+
+    // Check for and download any engine and pkg/ updates.
+    // We run the 'flutter' shell script re-entrantly here
+    // so that it will download the updated Dart and so forth
+    // if necessary.
+    printStatus('');
+    printStatus('Downloading engine...');
+    int code = await runCommandAndStreamOutput(<String>[
+      fs.path.join('bin', 'flutter'),
+      '--no-color',
+      'precache',
+    ], workingDirectory: Cache.flutterRoot, allowReentrantFlutter: true);
+
+    if (code != 0) {
+      throwToolExit(null, exitCode: code);
+    }
+
+    printStatus('');
+    printStatus(flutterVersion.toString());
+
+    final String projectRoot = findProjectRoot();
+    if (projectRoot != null) {
+      printStatus('');
+      await pubGet(
+        context: PubContext.pubUpgrade,
+        directory: projectRoot,
+        upgrade: true,
+        checkLastModified: false
+      );
+    }
+
+    // Run a doctor check in case system requirements have changed.
+    printStatus('');
+    printStatus('Running flutter doctor...');
+    code = await runCommandAndStreamOutput(
+      <String>[
+        fs.path.join('bin', 'flutter'),
+        'doctor',
+      ],
+      workingDirectory: Cache.flutterRoot,
+      allowReentrantFlutter: true,
+    );
+
+    if (code != 0) {
+      throwToolExit(null, exitCode: code);
+    }
+
+    return const FlutterCommandResult(ExitStatus.success);
+  }
+}
diff --git a/packages/flutter_tools/test/commands/version_test.dart b/packages/flutter_tools/test/commands/version_test.dart
new file mode 100644
index 0000000..aed04a2
--- /dev/null
+++ b/packages/flutter_tools/test/commands/version_test.dart
@@ -0,0 +1,121 @@
+// Copyright 2019 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart' show MockProcess;
+
+void main() {
+  group('version', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    testUsingContext('version ls', () async {
+      final VersionCommand command = VersionCommand();
+      await createTestCommandRunner(command).run(<String>['version']);
+      expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n' ''));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('version switch', () async {
+      const String version = '10.0.0';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.statusText, contains('Switching Flutter to version $version'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('switch to not supported version without force', () async {
+      const String version = '1.1.5';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.errorText, contains('Version command is not supported in'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('switch to not supported version with force', () async {
+      const String version = '1.1.5';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', '--force', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {
+  String version = '';
+
+  @override
+  Future<ProcessResult> run(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) async {
+    if (command[0] == 'git' && command[1] == 'tag') {
+      return ProcessResult(0, 0, 'v10.0.0\r\nv20.0.0', '');
+    }
+    if (command[0] == 'git' && command[1] == 'checkout') {
+      version = command[2];
+    }
+    return ProcessResult(0, 0, '', '');
+  }
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    final String commandStr = command.join(' ');
+    if (commandStr == 'git log -n 1 --pretty=format:%H') {
+      return ProcessResult(0, 0, '000000000000000000000', '');
+    }
+    if (commandStr ==
+        'git describe --match v*.*.* --first-parent --long --tags') {
+      if (version.isNotEmpty) {
+        return ProcessResult(0, 0, '$version-0-g00000000', '');
+      }
+    }
+    return ProcessResult(0, 0, '', '');
+  }
+
+  @override
+  Future<Process> start(List<dynamic> command,
+      {String workingDirectory,
+      Map<String, String> environment,
+      bool includeParentEnvironment = true,
+      bool runInShell = false,
+      ProcessStartMode mode = ProcessStartMode.normal}) {
+    final Completer<Process> completer = Completer<Process>();
+    completer.complete(MockProcess());
+    return completer.future;
+  }
+}