Include metadata in GitHub crash template (#53118)

diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index 4ab1ff6..5342f24 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -14,6 +14,7 @@
 import 'src/base/file_system.dart';
 import 'src/base/io.dart';
 import 'src/base/logger.dart';
+import 'src/base/net.dart';
 import 'src/base/process.dart';
 import 'src/context_runner.dart';
 import 'src/doctor.dart';
@@ -157,7 +158,13 @@
   globals.printError('A crash report has been written to ${file.path}.');
   globals.printStatus('This crash may already be reported. Check GitHub for similar crashes.', emphasis: true);
 
-  final GitHubTemplateCreator gitHubTemplateCreator = context.get<GitHubTemplateCreator>() ?? GitHubTemplateCreator();
+  final HttpClientFactory clientFactory = context.get<HttpClientFactory>();
+  final GitHubTemplateCreator gitHubTemplateCreator = context.get<GitHubTemplateCreator>() ?? GitHubTemplateCreator(
+    fileSystem: globals.fs,
+    logger: globals.logger,
+    flutterProjectFactory: globals.projectFactory,
+    client: clientFactory != null ? clientFactory() : HttpClient(),
+  );
   final String similarIssuesURL = await gitHubTemplateCreator.toolCrashSimilarIssuesGitHubURL(errorString);
   globals.printStatus('$similarIssuesURL\n', wrap: false);
   globals.printStatus('To report your crash to the Flutter team, first read the guide to filing a bug.', emphasis: true);
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 81e117a..2c73de4 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
-import 'package:yaml/yaml.dart' as yaml;
 
 import '../android/android.dart' as android;
 import '../android/android_sdk.dart' as android_sdk;
@@ -21,38 +20,13 @@
 import '../dart/pub.dart';
 import '../doctor.dart';
 import '../features.dart';
+import '../flutter_project_metadata.dart';
 import '../globals.dart' as globals;
 import '../project.dart';
 import '../reporting/reporting.dart';
 import '../runner/flutter_command.dart';
 import '../template.dart';
 
-enum _ProjectType {
-  /// This is the default project with the user-managed host code.
-  /// It is different than the "module" template in that it exposes and doesn't
-  /// manage the platform code.
-  app,
-  /// The is a project that has managed platform host code. It is an application with
-  /// ephemeral .ios and .android directories that can be updated automatically.
-  module,
-  /// This is a Flutter Dart package project. It doesn't have any native
-  /// components, only Dart.
-  package,
-  /// This is a native plugin project.
-  plugin,
-}
-
-_ProjectType _stringToProjectType(String value) {
-  _ProjectType result;
-  for (final _ProjectType type in _ProjectType.values) {
-    if (value == getEnumName(type)) {
-      result = type;
-      break;
-    }
-  }
-  return result;
-}
-
 class CreateCommand extends FlutterCommand {
   CreateCommand() {
     argParser.addFlag('pub',
@@ -74,17 +48,17 @@
     argParser.addOption(
       'template',
       abbr: 't',
-      allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
+      allowed: FlutterProjectType.values.map<String>((FlutterProjectType type) => type.name),
       help: 'Specify the type of project to create.',
       valueHelp: 'type',
       allowedHelp: <String, String>{
-        getEnumName(_ProjectType.app): '(default) Generate a Flutter application.',
-        getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
+        FlutterProjectType.app.name: '(default) Generate a Flutter application.',
+        FlutterProjectType.package.name: 'Generate a shareable Flutter project containing modular '
             'Dart code.',
-        getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
+        FlutterProjectType.plugin.name: 'Generate a shareable Flutter project containing an API '
             'in Dart code with a platform-specific implementation for Android, for iOS code, or '
             'for both.',
-        getEnumName(_ProjectType.module): 'Generate a project to add a Flutter module to an '
+        FlutterProjectType.module.name: 'Generate a project to add a Flutter module to an '
             'existing Android or iOS application.',
       },
       defaultsTo: null,
@@ -180,47 +154,24 @@
   // If it has an ios dir and an ios/Flutter dir, it's a legacy app
   // Otherwise, we don't presume to know what type of project it could be, since
   // many of the files could be missing, and we can't really tell definitively.
-  _ProjectType _determineTemplateType(Directory projectDir) {
-    yaml.YamlMap loadMetadata(Directory projectDir) {
-      if (!projectDir.existsSync()) {
-        return null;
-      }
-      final File metadataFile = globals.fs.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
-      if (!metadataFile.existsSync()) {
-        return null;
-      }
-      final dynamic metadataYaml = yaml.loadYaml(metadataFile.readAsStringSync());
-      if (metadataYaml is yaml.YamlMap) {
-        return metadataYaml;
-      } else {
-        throwToolExit('pubspec.yaml is malformed.');
-        return null;
-      }
+  FlutterProjectType _determineTemplateType(Directory projectDir) {
+    final File metadataFile = globals.fs.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
+    final FlutterProjectMetadata projectMetadata = FlutterProjectMetadata(metadataFile, globals.logger);
+    if (projectMetadata.projectType != null) {
+      return projectMetadata.projectType;
     }
 
     bool exists(List<String> path) {
       return globals.fs.directory(globals.fs.path.joinAll(<String>[projectDir.absolute.path, ...path])).existsSync();
     }
 
-    // If it exists, the project type in the metadata is definitive.
-    final yaml.YamlMap metadata = loadMetadata(projectDir);
-    if (metadata != null && metadata['project_type'] != null) {
-      final dynamic projectType = metadata['project_type'];
-      if (projectType is String) {
-        return _stringToProjectType(projectType);
-      } else {
-        throwToolExit('.metadata is malformed.');
-        return null;
-      }
-    }
-
     // There either wasn't any metadata, or it didn't contain the project type,
     // so try and figure out what type of project it is from the existing
     // directory structure.
     if (exists(<String>['android', 'app'])
         || exists(<String>['ios', 'Runner'])
         || exists(<String>['ios', 'Flutter'])) {
-      return _ProjectType.app;
+      return FlutterProjectType.app;
     }
     // Since we can't really be definitive on nearly-empty directories, err on
     // the side of prudence and just say we don't know.
@@ -277,12 +228,12 @@
     }
   }
 
-  _ProjectType _getProjectType(Directory projectDir) {
-    _ProjectType template;
-    _ProjectType detectedProjectType;
+  FlutterProjectType _getProjectType(Directory projectDir) {
+    FlutterProjectType template;
+    FlutterProjectType detectedProjectType;
     final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
     if (argResults['template'] != null) {
-      template = _stringToProjectType(stringArg('template'));
+      template = stringToProjectType(stringArg('template'));
     } else {
       // If the project directory exists and isn't empty, then try to determine the template
       // type from the project directory.
@@ -297,12 +248,12 @@
         }
       }
     }
-    template ??= detectedProjectType ?? _ProjectType.app;
+    template ??= detectedProjectType ?? FlutterProjectType.app;
     if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
       // We can only be definitive that this is the wrong type if the .metadata file
       // exists and contains a type that doesn't match.
-      throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
-          "existing template type of '${getEnumName(detectedProjectType)}'.");
+      throwToolExit("The requested template type '${template.name}' doesn't match the "
+          "existing template type of '${detectedProjectType.name}'.");
     }
     return template;
   }
@@ -356,18 +307,18 @@
     String sampleCode;
     if (argResults['sample'] != null) {
       if (argResults['template'] != null &&
-        _stringToProjectType(stringArg('template') ?? 'app') != _ProjectType.app) {
+        stringToProjectType(stringArg('template') ?? 'app') != FlutterProjectType.app) {
         throwToolExit('Cannot specify --sample with a project type other than '
-          '"${getEnumName(_ProjectType.app)}"');
+          '"${FlutterProjectType.app.name}"');
       }
       // Fetch the sample from the server.
       sampleCode = await _fetchSampleFromServer(stringArg('sample'));
     }
 
-    final _ProjectType template = _getProjectType(projectDir);
-    final bool generateModule = template == _ProjectType.module;
-    final bool generatePlugin = template == _ProjectType.plugin;
-    final bool generatePackage = template == _ProjectType.package;
+    final FlutterProjectType template = _getProjectType(projectDir);
+    final bool generateModule = template == FlutterProjectType.module;
+    final bool generatePlugin = template == FlutterProjectType.plugin;
+    final bool generatePackage = template == FlutterProjectType.package;
 
     String organization = stringArg('org');
     if (!argResults.wasParsed('org')) {
@@ -424,16 +375,16 @@
     final Directory relativeDir = globals.fs.directory(projectDirPath);
     int generatedFileCount = 0;
     switch (template) {
-      case _ProjectType.app:
+      case FlutterProjectType.app:
         generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: overwrite);
         break;
-      case _ProjectType.module:
+      case FlutterProjectType.module:
         generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: overwrite);
         break;
-      case _ProjectType.package:
+      case FlutterProjectType.package:
         generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: overwrite);
         break;
-      case _ProjectType.plugin:
+      case FlutterProjectType.plugin:
         generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: overwrite);
         break;
     }
diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart
new file mode 100644
index 0000000..a6d9414
--- /dev/null
+++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart
@@ -0,0 +1,99 @@
+// 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:yaml/yaml.dart';
+
+import 'base/file_system.dart';
+import 'base/logger.dart';
+import 'base/utils.dart';
+
+enum FlutterProjectType {
+  /// This is the default project with the user-managed host code.
+  /// It is different than the "module" template in that it exposes and doesn't
+  /// manage the platform code.
+  app,
+  /// The is a project that has managed platform host code. It is an application with
+  /// ephemeral .ios and .android directories that can be updated automatically.
+  module,
+  /// This is a Flutter Dart package project. It doesn't have any native
+  /// components, only Dart.
+  package,
+  /// This is a native plugin project.
+  plugin,
+}
+
+extension FlutterProjectTypeExtension on FlutterProjectType {
+  String get name => getEnumName(this);
+}
+
+FlutterProjectType stringToProjectType(String value) {
+  FlutterProjectType result;
+  for (final FlutterProjectType type in FlutterProjectType.values) {
+    if (value == type.name) {
+      result = type;
+      break;
+    }
+  }
+  return result;
+}
+
+/// A wrapper around the `.metadata` file.
+class FlutterProjectMetadata {
+  FlutterProjectMetadata(
+    File metadataFile,
+    Logger logger,
+    )  : _metadataFile = metadataFile,
+      _logger = logger;
+
+  final File _metadataFile;
+  final Logger _logger;
+
+  String get versionChannel => _versionValue('channel');
+  String get versionRevision => _versionValue('revision');
+
+  FlutterProjectType get projectType {
+    final dynamic projectTypeYaml = _metadataValue('project_type');
+    if (projectTypeYaml is String) {
+      return stringToProjectType(projectTypeYaml);
+    } else {
+      _logger.printTrace('.metadata project_type version is malformed.');
+      return null;
+    }
+  }
+
+  YamlMap _versionYaml;
+  String _versionValue(String key) {
+    if (_versionYaml == null) {
+      final dynamic versionYaml = _metadataValue('version');
+      if (versionYaml is YamlMap) {
+        _versionYaml = versionYaml;
+      } else {
+        _logger.printTrace('.metadata version is malformed.');
+        return null;
+      }
+    }
+    if (_versionYaml != null && _versionYaml.containsKey(key) && _versionYaml[key] is String) {
+      return _versionYaml[key] as String;
+    }
+    return null;
+  }
+
+  YamlMap _metadataYaml;
+  dynamic _metadataValue(String key) {
+    if (_metadataYaml == null) {
+      if (!_metadataFile.existsSync()) {
+        return null;
+      }
+      final dynamic metadataYaml = loadYaml(_metadataFile.readAsStringSync());
+      if (metadataYaml is YamlMap) {
+        _metadataYaml = metadataYaml;
+      } else {
+        _logger.printTrace('.metadata is malformed.');
+        return null;
+      }
+    }
+
+    return _metadataYaml[key];
+  }
+}
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index 4d77793..69c8311 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -30,6 +30,7 @@
 import 'macos/cocoapods.dart';
 import 'macos/xcode.dart';
 import 'persistent_tool_state.dart';
+import 'project.dart';
 import 'reporting/reporting.dart';
 import 'version.dart';
 import 'web/chrome.dart';
@@ -42,6 +43,7 @@
 OperatingSystemUtils get os => context.get<OperatingSystemUtils>();
 PersistentToolState get persistentToolState => PersistentToolState.instance;
 Usage get flutterUsage => context.get<Usage>();
+FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
 
 const FileSystem _kLocalFs = LocalFileSystem();
 
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 2926a18..4efd7c5 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -11,7 +11,6 @@
 import 'android/gradle_utils.dart' as gradle;
 import 'artifacts.dart';
 import 'base/common.dart';
-import 'base/context.dart';
 import 'base/file_system.dart';
 import 'build_info.dart';
 import 'bundle.dart' as bundle;
@@ -24,8 +23,6 @@
 import 'plugins.dart';
 import 'template.dart';
 
-FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
-
 class FlutterProjectFactory {
   FlutterProjectFactory();
 
@@ -69,7 +66,7 @@
 
   /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
   /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
-  static FlutterProject fromDirectory(Directory directory) => projectFactory.fromDirectory(directory);
+  static FlutterProject fromDirectory(Directory directory) => globals.projectFactory.fromDirectory(directory);
 
   /// Returns a [FlutterProject] view of the current directory or a ToolExit error,
   /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
@@ -145,6 +142,9 @@
   /// The `.packages` file of this project.
   File get packagesFile => directory.childFile('.packages');
 
+  /// The `.metadata` file of this project.
+  File get metadataFile => directory.childFile('.metadata');
+
   /// The `.flutter-plugins` file of this project.
   File get flutterPluginsFile => directory.childFile('.flutter-plugins');
 
diff --git a/packages/flutter_tools/lib/src/reporting/github_template.dart b/packages/flutter_tools/lib/src/reporting/github_template.dart
index 5d958b7..76a1bd3 100644
--- a/packages/flutter_tools/lib/src/reporting/github_template.dart
+++ b/packages/flutter_tools/lib/src/reporting/github_template.dart
@@ -5,23 +5,31 @@
 import 'dart:async';
 
 import 'package:file/file.dart';
+import 'package:meta/meta.dart';
 
-import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
-import '../base/net.dart';
+import '../base/logger.dart';
 import '../convert.dart';
 import '../flutter_manifest.dart';
-import '../globals.dart' as globals;
+import '../flutter_project_metadata.dart';
 import '../project.dart';
 
 /// Provide suggested GitHub issue templates to user when Flutter encounters an error.
 class GitHubTemplateCreator {
-  GitHubTemplateCreator() :
-      _client = (context.get<HttpClientFactory>() == null)
-        ? HttpClient()
-        : context.get<HttpClientFactory>()();
+  GitHubTemplateCreator({
+    @required FileSystem fileSystem,
+    @required Logger logger,
+    @required FlutterProjectFactory flutterProjectFactory,
+    @required HttpClient client,
+  }) : _fileSystem = fileSystem,
+      _logger = logger,
+      _flutterProjectFactory = flutterProjectFactory,
+      _client = client;
 
+  final FileSystem _fileSystem;
+  final Logger _logger;
+  final FlutterProjectFactory _flutterProjectFactory;
   final HttpClient _client;
 
   Future<String> toolCrashSimilarIssuesGitHubURL(String errorString) async {
@@ -76,7 +84,7 @@
   String _projectMetadataInformation() {
     FlutterProject project;
     try {
-      project = FlutterProject.current();
+      project = _flutterProjectFactory.fromDirectory(_fileSystem.currentDirectory);
     } on Exception catch (exception) {
       // pubspec may be malformed.
       return exception.toString();
@@ -86,14 +94,18 @@
       if (project == null || manifest == null || manifest.isEmpty) {
         return 'No pubspec in working directory.';
       }
+      final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.metadataFile, _logger);
       final StringBuffer description = StringBuffer()
+        ..writeln('**Type**: ${metadata.projectType?.name}')
         ..writeln('**Version**: ${manifest.appVersion}')
         ..writeln('**Material**: ${manifest.usesMaterialDesign}')
         ..writeln('**Android X**: ${manifest.usesAndroidX}')
         ..writeln('**Module**: ${manifest.isModule}')
         ..writeln('**Plugin**: ${manifest.isPlugin}')
         ..writeln('**Android package**: ${manifest.androidPackage}')
-        ..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}');
+        ..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}')
+        ..writeln('**Creation channel**: ${metadata.versionChannel}')
+        ..writeln('**Creation framework version**: ${metadata.versionRevision}');
 
       final File file = project.flutterPluginsFile;
       if (file.existsSync()) {
@@ -107,7 +119,7 @@
           }
           // Write the last part of the path, which includes the plugin name and version.
           // Example: camera-0.5.7+2
-          final List<String> pathParts = globals.fs.path.split(pluginParts[1]);
+          final List<String> pathParts = _fileSystem.path.split(pluginParts[1]);
           description.writeln(pathParts.isEmpty ? pluginParts.first : pathParts.last);
         }
       }
@@ -124,7 +136,7 @@
   Future<String> _shortURL(String fullURL) async {
     String url;
     try {
-      globals.printTrace('Attempting git.io shortener: $fullURL');
+      _logger.printTrace('Attempting git.io shortener: $fullURL');
       final List<int> bodyBytes = utf8.encode('url=${Uri.encodeQueryComponent(fullURL)}');
       final HttpClientRequest request = await _client.postUrl(Uri.parse('https://git.io'));
       request.headers.set(HttpHeaders.contentLengthHeader, bodyBytes.length.toString());
@@ -134,10 +146,10 @@
       if (response.statusCode == 201) {
         url = response.headers[HttpHeaders.locationHeader]?.first;
       } else {
-        globals.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
+        _logger.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
       }
     } on Exception catch (sendError) {
-      globals.printTrace('Failed to shorten GitHub template URL: $sendError');
+      _logger.printTrace('Failed to shorten GitHub template URL: $sendError');
     }
 
     return url ?? fullURL;