mirror of
https://github.com/flutter/packages.git
synced 2025-06-01 06:18:12 +08:00

- Updates dependencies to null-safe versions - Migrates common.dart (which doesn't depend on anything) - Migrates common_tests.dart and its one dependency, utils.dart - Adds build_runner for Mockito mock generation - Adds a new utility methods for getting arguments that handle both the casting and the removal of nullability to address a common problematic pattern while migrating code. - Converts all files, not just the migrated ones, to those new helpers. Migrating common.dart and utils.dart should unblock a command-by-command migration to null safety. Reverts the separate of podspect lints into a step that doesn't do a Flutter upgrade (https://github.com/flutter/plugins/pull/3700) because without that step we had a version of Dart too old to run null-safe tooling. First step of https://github.com/flutter/flutter/issues/81912
742 lines
26 KiB
Dart
742 lines
26 KiB
Dart
// Copyright 2013 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 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io' as io;
|
|
import 'dart:math';
|
|
|
|
import 'package:args/command_runner.dart';
|
|
import 'package:colorize/colorize.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:git/git.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
/// The signature for a print handler for commands that allow overriding the
|
|
/// print destination.
|
|
typedef Print = void Function(Object? object);
|
|
|
|
/// Key for windows platform.
|
|
const String kWindows = 'windows';
|
|
|
|
/// Key for macos platform.
|
|
const String kMacos = 'macos';
|
|
|
|
/// Key for linux platform.
|
|
const String kLinux = 'linux';
|
|
|
|
/// Key for IPA (iOS) platform.
|
|
const String kIos = 'ios';
|
|
|
|
/// Key for APK (Android) platform.
|
|
const String kAndroid = 'android';
|
|
|
|
/// Key for Web platform.
|
|
const String kWeb = 'web';
|
|
|
|
/// Key for IPA.
|
|
const String kIpa = 'ipa';
|
|
|
|
/// Key for APK.
|
|
const String kApk = 'apk';
|
|
|
|
/// Key for enable experiment.
|
|
const String kEnableExperiment = 'enable-experiment';
|
|
|
|
/// Returns whether the given directory contains a Flutter package.
|
|
bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) {
|
|
if (entity is! Directory) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
final File pubspecFile =
|
|
fileSystem.file(p.join(entity.path, 'pubspec.yaml'));
|
|
final YamlMap pubspecYaml =
|
|
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
|
|
final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?;
|
|
if (dependencies == null) {
|
|
return false;
|
|
}
|
|
return dependencies.containsKey('flutter');
|
|
} on FileSystemException {
|
|
return false;
|
|
} on YamlException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter [platform] plugin.
|
|
///
|
|
/// It checks this by looking for the following pattern in the pubspec:
|
|
///
|
|
/// flutter:
|
|
/// plugin:
|
|
/// platforms:
|
|
/// [platform]:
|
|
bool pluginSupportsPlatform(
|
|
String platform, FileSystemEntity entity, FileSystem fileSystem) {
|
|
assert(platform == kIos ||
|
|
platform == kAndroid ||
|
|
platform == kWeb ||
|
|
platform == kMacos ||
|
|
platform == kWindows ||
|
|
platform == kLinux);
|
|
if (entity is! Directory) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
final File pubspecFile =
|
|
fileSystem.file(p.join(entity.path, 'pubspec.yaml'));
|
|
final YamlMap pubspecYaml =
|
|
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
|
|
final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?;
|
|
if (flutterSection == null) {
|
|
return false;
|
|
}
|
|
final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?;
|
|
if (pluginSection == null) {
|
|
return false;
|
|
}
|
|
final YamlMap? platforms = pluginSection['platforms'] as YamlMap?;
|
|
if (platforms == null) {
|
|
// Legacy plugin specs are assumed to support iOS and Android.
|
|
if (!pluginSection.containsKey('platforms')) {
|
|
return platform == kIos || platform == kAndroid;
|
|
}
|
|
return false;
|
|
}
|
|
return platforms.containsKey(platform);
|
|
} on FileSystemException {
|
|
return false;
|
|
} on YamlException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter Android plugin.
|
|
bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kAndroid, entity, fileSystem);
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter iOS plugin.
|
|
bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kIos, entity, fileSystem);
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter web plugin.
|
|
bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kWeb, entity, fileSystem);
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter Windows plugin.
|
|
bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kWindows, entity, fileSystem);
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter macOS plugin.
|
|
bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kMacos, entity, fileSystem);
|
|
}
|
|
|
|
/// Returns whether the given directory contains a Flutter linux plugin.
|
|
bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) {
|
|
return pluginSupportsPlatform(kLinux, entity, fileSystem);
|
|
}
|
|
|
|
/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red.
|
|
void printErrorAndExit({required String errorMessage, int exitCode = 1}) {
|
|
final Colorize redError = Colorize(errorMessage)..red();
|
|
print(redError);
|
|
throw ToolExit(exitCode);
|
|
}
|
|
|
|
/// Error thrown when a command needs to exit with a non-zero exit code.
|
|
class ToolExit extends Error {
|
|
/// Creates a tool exit with the given [exitCode].
|
|
ToolExit(this.exitCode);
|
|
|
|
/// The code that the process should exit with.
|
|
final int exitCode;
|
|
}
|
|
|
|
/// Interface definition for all commands in this tool.
|
|
abstract class PluginCommand extends Command<void> {
|
|
/// Creates a command to operate on [packagesDir] with the given environment.
|
|
PluginCommand(
|
|
this.packagesDir,
|
|
this.fileSystem, {
|
|
this.processRunner = const ProcessRunner(),
|
|
this.gitDir,
|
|
}) {
|
|
argParser.addMultiOption(
|
|
_pluginsArg,
|
|
splitCommas: true,
|
|
help:
|
|
'Specifies which plugins the command should run on (before sharding).',
|
|
valueHelp: 'plugin1,plugin2,...',
|
|
);
|
|
argParser.addOption(
|
|
_shardIndexArg,
|
|
help: 'Specifies the zero-based index of the shard to '
|
|
'which the command applies.',
|
|
valueHelp: 'i',
|
|
defaultsTo: '0',
|
|
);
|
|
argParser.addOption(
|
|
_shardCountArg,
|
|
help: 'Specifies the number of shards into which plugins are divided.',
|
|
valueHelp: 'n',
|
|
defaultsTo: '1',
|
|
);
|
|
argParser.addMultiOption(
|
|
_excludeArg,
|
|
abbr: 'e',
|
|
help: 'Exclude packages from this command.',
|
|
defaultsTo: <String>[],
|
|
);
|
|
argParser.addFlag(_runOnChangedPackagesArg,
|
|
help: 'Run the command on changed packages/plugins.\n'
|
|
'If the $_pluginsArg is specified, this flag is ignored.\n'
|
|
'If no plugins have changed, the command runs on all plugins.\n'
|
|
'The packages excluded with $_excludeArg is also excluded even if changed.\n'
|
|
'See $_kBaseSha if a custom base is needed to determine the diff.');
|
|
argParser.addOption(_kBaseSha,
|
|
help: 'The base sha used to determine git diff. \n'
|
|
'This is useful when $_runOnChangedPackagesArg is specified.\n'
|
|
'If not specified, merge-base is used as base sha.');
|
|
}
|
|
|
|
static const String _pluginsArg = 'plugins';
|
|
static const String _shardIndexArg = 'shardIndex';
|
|
static const String _shardCountArg = 'shardCount';
|
|
static const String _excludeArg = 'exclude';
|
|
static const String _runOnChangedPackagesArg = 'run-on-changed-packages';
|
|
static const String _kBaseSha = 'base-sha';
|
|
|
|
/// The directory containing the plugin packages.
|
|
final Directory packagesDir;
|
|
|
|
/// The file system.
|
|
///
|
|
/// This can be overridden for testing.
|
|
final FileSystem fileSystem;
|
|
|
|
/// The process runner.
|
|
///
|
|
/// This can be overridden for testing.
|
|
final ProcessRunner processRunner;
|
|
|
|
/// The git directory to use. By default it uses the parent directory.
|
|
///
|
|
/// This can be mocked for testing.
|
|
final GitDir? gitDir;
|
|
|
|
int? _shardIndex;
|
|
int? _shardCount;
|
|
|
|
/// The shard of the overall command execution that this instance should run.
|
|
int get shardIndex {
|
|
if (_shardIndex == null) {
|
|
_checkSharding();
|
|
}
|
|
return _shardIndex!;
|
|
}
|
|
|
|
/// The number of shards this command is divided into.
|
|
int get shardCount {
|
|
if (_shardCount == null) {
|
|
_checkSharding();
|
|
}
|
|
return _shardCount!;
|
|
}
|
|
|
|
/// Convenience accessor for boolean arguments.
|
|
bool getBoolArg(String key) {
|
|
return (argResults![key] as bool?) ?? false;
|
|
}
|
|
|
|
/// Convenience accessor for String arguments.
|
|
String getStringArg(String key) {
|
|
return (argResults![key] as String?) ?? '';
|
|
}
|
|
|
|
/// Convenience accessor for List<String> arguments.
|
|
List<String> getStringListArg(String key) {
|
|
return (argResults![key] as List<String>?) ?? <String>[];
|
|
}
|
|
|
|
void _checkSharding() {
|
|
final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg));
|
|
final int? shardCount = int.tryParse(getStringArg(_shardCountArg));
|
|
if (shardIndex == null) {
|
|
usageException('$_shardIndexArg must be an integer');
|
|
}
|
|
if (shardCount == null) {
|
|
usageException('$_shardCountArg must be an integer');
|
|
}
|
|
if (shardCount < 1) {
|
|
usageException('$_shardCountArg must be positive');
|
|
}
|
|
if (shardIndex < 0 || shardCount <= shardIndex) {
|
|
usageException(
|
|
'$_shardIndexArg must be in the half-open range [0..$shardCount[');
|
|
}
|
|
_shardIndex = shardIndex;
|
|
_shardCount = shardCount;
|
|
}
|
|
|
|
/// Returns the root Dart package folders of the plugins involved in this
|
|
/// command execution.
|
|
Stream<Directory> getPlugins() async* {
|
|
// To avoid assuming consistency of `Directory.list` across command
|
|
// invocations, we collect and sort the plugin folders before sharding.
|
|
// This is considered an implementation detail which is why the API still
|
|
// uses streams.
|
|
final List<Directory> allPlugins = await _getAllPlugins().toList();
|
|
allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path));
|
|
// Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2.
|
|
// Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3.
|
|
// Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0.
|
|
final int shardSize = allPlugins.length ~/ shardCount +
|
|
(allPlugins.length % shardCount == 0 ? 0 : 1);
|
|
final int start = min(shardIndex * shardSize, allPlugins.length);
|
|
final int end = min(start + shardSize, allPlugins.length);
|
|
|
|
for (final Directory plugin in allPlugins.sublist(start, end)) {
|
|
yield plugin;
|
|
}
|
|
}
|
|
|
|
/// Returns the root Dart package folders of the plugins involved in this
|
|
/// command execution, assuming there is only one shard.
|
|
///
|
|
/// Plugin packages can exist in the following places relative to the packages
|
|
/// directory:
|
|
///
|
|
/// 1. As a Dart package in a directory which is a direct child of the
|
|
/// packages directory. This is a plugin where all of the implementations
|
|
/// exist in a single Dart package.
|
|
/// 2. Several plugin packages may live in a directory which is a direct
|
|
/// child of the packages directory. This directory groups several Dart
|
|
/// packages which implement a single plugin. This directory contains a
|
|
/// "client library" package, which declares the API for the plugin, as
|
|
/// well as one or more platform-specific implementations.
|
|
/// 3./4. Either of the above, but in a third_party/packages/ directory that
|
|
/// is a sibling of the packages directory. This is used for a small number
|
|
/// of packages in the flutter/packages repository.
|
|
Stream<Directory> _getAllPlugins() async* {
|
|
Set<String> plugins = Set<String>.from(getStringListArg(_pluginsArg));
|
|
final Set<String> excludedPlugins =
|
|
Set<String>.from(getStringListArg(_excludeArg));
|
|
final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg);
|
|
if (plugins.isEmpty && runOnChangedPackages) {
|
|
plugins = await _getChangedPackages();
|
|
}
|
|
|
|
final Directory thirdPartyPackagesDirectory = packagesDir.parent
|
|
.childDirectory('third_party')
|
|
.childDirectory('packages');
|
|
|
|
for (final Directory dir in <Directory>[
|
|
packagesDir,
|
|
if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory,
|
|
]) {
|
|
await for (final FileSystemEntity entity
|
|
in dir.list(followLinks: false)) {
|
|
// A top-level Dart package is a plugin package.
|
|
if (_isDartPackage(entity)) {
|
|
if (!excludedPlugins.contains(entity.basename) &&
|
|
(plugins.isEmpty || plugins.contains(p.basename(entity.path)))) {
|
|
yield entity as Directory;
|
|
}
|
|
} else if (entity is Directory) {
|
|
// Look for Dart packages under this top-level directory.
|
|
await for (final FileSystemEntity subdir
|
|
in entity.list(followLinks: false)) {
|
|
if (_isDartPackage(subdir)) {
|
|
// If --plugin=my_plugin is passed, then match all federated
|
|
// plugins under 'my_plugin'. Also match if the exact plugin is
|
|
// passed.
|
|
final String relativePath =
|
|
p.relative(subdir.path, from: dir.path);
|
|
final String packageName = p.basename(subdir.path);
|
|
final String basenamePath = p.basename(entity.path);
|
|
if (!excludedPlugins.contains(basenamePath) &&
|
|
!excludedPlugins.contains(packageName) &&
|
|
!excludedPlugins.contains(relativePath) &&
|
|
(plugins.isEmpty ||
|
|
plugins.contains(relativePath) ||
|
|
plugins.contains(basenamePath))) {
|
|
yield subdir as Directory;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the example Dart package folders of the plugins involved in this
|
|
/// command execution.
|
|
Stream<Directory> getExamples() =>
|
|
getPlugins().expand<Directory>(getExamplesForPlugin);
|
|
|
|
/// Returns all Dart package folders (typically, plugin + example) of the
|
|
/// plugins involved in this command execution.
|
|
Stream<Directory> getPackages() async* {
|
|
await for (final Directory plugin in getPlugins()) {
|
|
yield plugin;
|
|
yield* plugin
|
|
.list(recursive: true, followLinks: false)
|
|
.where(_isDartPackage)
|
|
.cast<Directory>();
|
|
}
|
|
}
|
|
|
|
/// Returns the files contained, recursively, within the plugins
|
|
/// involved in this command execution.
|
|
Stream<File> getFiles() {
|
|
return getPlugins().asyncExpand<File>((Directory folder) => folder
|
|
.list(recursive: true, followLinks: false)
|
|
.where((FileSystemEntity entity) => entity is File)
|
|
.cast<File>());
|
|
}
|
|
|
|
/// Returns whether the specified entity is a directory containing a
|
|
/// `pubspec.yaml` file.
|
|
bool _isDartPackage(FileSystemEntity entity) {
|
|
return entity is Directory &&
|
|
fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync();
|
|
}
|
|
|
|
/// Returns the example Dart packages contained in the specified plugin, or
|
|
/// an empty List, if the plugin has no examples.
|
|
Iterable<Directory> getExamplesForPlugin(Directory plugin) {
|
|
final Directory exampleFolder =
|
|
fileSystem.directory(p.join(plugin.path, 'example'));
|
|
if (!exampleFolder.existsSync()) {
|
|
return <Directory>[];
|
|
}
|
|
if (isFlutterPackage(exampleFolder, fileSystem)) {
|
|
return <Directory>[exampleFolder];
|
|
}
|
|
// Only look at the subdirectories of the example directory if the example
|
|
// directory itself is not a Dart package, and only look one level below the
|
|
// example directory for other dart packages.
|
|
return exampleFolder
|
|
.listSync()
|
|
.where(
|
|
(FileSystemEntity entity) => isFlutterPackage(entity, fileSystem))
|
|
.cast<Directory>();
|
|
}
|
|
|
|
/// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir].
|
|
///
|
|
/// Throws tool exit if [gitDir] nor root directory is a git directory.
|
|
Future<GitVersionFinder> retrieveVersionFinder() async {
|
|
final String rootDir = packagesDir.parent.absolute.path;
|
|
final String baseSha = getStringArg(_kBaseSha);
|
|
|
|
GitDir? baseGitDir = gitDir;
|
|
if (baseGitDir == null) {
|
|
if (!await GitDir.isGitDir(rootDir)) {
|
|
printErrorAndExit(
|
|
errorMessage: '$rootDir is not a valid Git repository.',
|
|
exitCode: 2);
|
|
}
|
|
baseGitDir = await GitDir.fromExisting(rootDir);
|
|
}
|
|
|
|
final GitVersionFinder gitVersionFinder =
|
|
GitVersionFinder(baseGitDir, baseSha);
|
|
return gitVersionFinder;
|
|
}
|
|
|
|
Future<Set<String>> _getChangedPackages() async {
|
|
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
|
|
|
final List<String> allChangedFiles =
|
|
await gitVersionFinder.getChangedFiles();
|
|
final Set<String> packages = <String>{};
|
|
for (final String path in allChangedFiles) {
|
|
final List<String> pathComponents = path.split('/');
|
|
final int packagesIndex =
|
|
pathComponents.indexWhere((String element) => element == 'packages');
|
|
if (packagesIndex != -1) {
|
|
packages.add(pathComponents[packagesIndex + 1]);
|
|
}
|
|
}
|
|
if (packages.isNotEmpty) {
|
|
final String changedPackages = packages.join(',');
|
|
print(changedPackages);
|
|
} else {
|
|
print('No changed packages.');
|
|
}
|
|
return packages;
|
|
}
|
|
}
|
|
|
|
/// A class used to run processes.
|
|
///
|
|
/// We use this instead of directly running the process so it can be overridden
|
|
/// in tests.
|
|
class ProcessRunner {
|
|
/// Creates a new process runner.
|
|
const ProcessRunner();
|
|
|
|
/// Run the [executable] with [args] and stream output to stderr and stdout.
|
|
///
|
|
/// The current working directory of [executable] can be overridden by
|
|
/// passing [workingDir].
|
|
///
|
|
/// If [exitOnError] is set to `true`, then this will throw an error if
|
|
/// the [executable] terminates with a non-zero exit code.
|
|
///
|
|
/// Returns the exit code of the [executable].
|
|
Future<int> runAndStream(
|
|
String executable,
|
|
List<String> args, {
|
|
Directory? workingDir,
|
|
bool exitOnError = false,
|
|
}) async {
|
|
print(
|
|
'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}');
|
|
final io.Process process = await io.Process.start(executable, args,
|
|
workingDirectory: workingDir?.path);
|
|
await io.stdout.addStream(process.stdout);
|
|
await io.stderr.addStream(process.stderr);
|
|
if (exitOnError && await process.exitCode != 0) {
|
|
final String error =
|
|
_getErrorString(executable, args, workingDir: workingDir);
|
|
print('$error See above for details.');
|
|
throw ToolExit(await process.exitCode);
|
|
}
|
|
return process.exitCode;
|
|
}
|
|
|
|
/// Run the [executable] with [args].
|
|
///
|
|
/// The current working directory of [executable] can be overridden by
|
|
/// passing [workingDir].
|
|
///
|
|
/// If [exitOnError] is set to `true`, then this will throw an error if
|
|
/// the [executable] terminates with a non-zero exit code.
|
|
/// Defaults to `false`.
|
|
///
|
|
/// If [logOnError] is set to `true`, it will print a formatted message about the error.
|
|
/// Defaults to `false`
|
|
///
|
|
/// Returns the [io.ProcessResult] of the [executable].
|
|
Future<io.ProcessResult> run(String executable, List<String> args,
|
|
{Directory? workingDir,
|
|
bool exitOnError = false,
|
|
bool logOnError = false,
|
|
Encoding stdoutEncoding = io.systemEncoding,
|
|
Encoding stderrEncoding = io.systemEncoding}) async {
|
|
final io.ProcessResult result = await io.Process.run(executable, args,
|
|
workingDirectory: workingDir?.path,
|
|
stdoutEncoding: stdoutEncoding,
|
|
stderrEncoding: stderrEncoding);
|
|
if (result.exitCode != 0) {
|
|
if (logOnError) {
|
|
final String error =
|
|
_getErrorString(executable, args, workingDir: workingDir);
|
|
print('$error Stderr:\n${result.stdout}');
|
|
}
|
|
if (exitOnError) {
|
|
throw ToolExit(result.exitCode);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Starts the [executable] with [args].
|
|
///
|
|
/// The current working directory of [executable] can be overridden by
|
|
/// passing [workingDir].
|
|
///
|
|
/// Returns the started [io.Process].
|
|
Future<io.Process?> start(String executable, List<String> args,
|
|
{Directory? workingDirectory}) async {
|
|
final io.Process process = await io.Process.start(executable, args,
|
|
workingDirectory: workingDirectory?.path);
|
|
return process;
|
|
}
|
|
|
|
String _getErrorString(String executable, List<String> args,
|
|
{Directory? workingDir}) {
|
|
final String workdir = workingDir == null ? '' : ' in ${workingDir.path}';
|
|
return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.';
|
|
}
|
|
}
|
|
|
|
/// Finding version of [package] that is published on pub.
|
|
class PubVersionFinder {
|
|
/// Constructor.
|
|
///
|
|
/// Note: you should manually close the [httpClient] when done using the finder.
|
|
PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient});
|
|
|
|
/// The default pub host to use.
|
|
static const String defaultPubHost = 'https://pub.dev';
|
|
|
|
/// The pub host url, defaults to `https://pub.dev`.
|
|
final String pubHost;
|
|
|
|
/// The http client.
|
|
///
|
|
/// You should manually close this client when done using this finder.
|
|
final http.Client httpClient;
|
|
|
|
/// Get the package version on pub.
|
|
Future<PubVersionFinderResponse> getPackageVersion(
|
|
{required String package}) async {
|
|
assert(package.isNotEmpty);
|
|
final Uri pubHostUri = Uri.parse(pubHost);
|
|
final Uri url = pubHostUri.replace(path: '/packages/$package.json');
|
|
final http.Response response = await httpClient.get(url);
|
|
|
|
if (response.statusCode == 404) {
|
|
return PubVersionFinderResponse(
|
|
versions: null,
|
|
result: PubVersionFinderResult.noPackageFound,
|
|
httpResponse: response);
|
|
} else if (response.statusCode != 200) {
|
|
return PubVersionFinderResponse(
|
|
versions: null,
|
|
result: PubVersionFinderResult.fail,
|
|
httpResponse: response);
|
|
}
|
|
final List<Version> versions =
|
|
(json.decode(response.body)['versions'] as List<dynamic>)
|
|
.map<Version>((final dynamic versionString) =>
|
|
Version.parse(versionString as String))
|
|
.toList();
|
|
|
|
return PubVersionFinderResponse(
|
|
versions: versions,
|
|
result: PubVersionFinderResult.success,
|
|
httpResponse: response);
|
|
}
|
|
}
|
|
|
|
/// Represents a response for [PubVersionFinder].
|
|
class PubVersionFinderResponse {
|
|
/// Constructor.
|
|
PubVersionFinderResponse({this.versions, this.result, this.httpResponse}) {
|
|
if (versions != null && versions!.isNotEmpty) {
|
|
versions!.sort((Version a, Version b) {
|
|
// TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize].
|
|
// https://github.com/flutter/flutter/issues/82222
|
|
return b.compareTo(a);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// The versions found in [PubVersionFinder].
|
|
///
|
|
/// This is sorted by largest to smallest, so the first element in the list is the largest version.
|
|
/// Might be `null` if the [result] is not [PubVersionFinderResult.success].
|
|
final List<Version>? versions;
|
|
|
|
/// The result of the version finder.
|
|
final PubVersionFinderResult? result;
|
|
|
|
/// The response object of the http request.
|
|
final http.Response? httpResponse;
|
|
}
|
|
|
|
/// An enum representing the result of [PubVersionFinder].
|
|
enum PubVersionFinderResult {
|
|
/// The version finder successfully found a version.
|
|
success,
|
|
|
|
/// The version finder failed to find a valid version.
|
|
///
|
|
/// This might due to http connection errors or user errors.
|
|
fail,
|
|
|
|
/// The version finder failed to locate the package.
|
|
///
|
|
/// This indicates the package is new.
|
|
noPackageFound,
|
|
}
|
|
|
|
/// Finding diffs based on `baseGitDir` and `baseSha`.
|
|
class GitVersionFinder {
|
|
/// Constructor
|
|
GitVersionFinder(this.baseGitDir, this.baseSha);
|
|
|
|
/// The top level directory of the git repo.
|
|
///
|
|
/// That is where the .git/ folder exists.
|
|
final GitDir baseGitDir;
|
|
|
|
/// The base sha used to get diff.
|
|
final String? baseSha;
|
|
|
|
static bool _isPubspec(String file) {
|
|
return file.trim().endsWith('pubspec.yaml');
|
|
}
|
|
|
|
/// Get a list of all the pubspec.yaml file that is changed.
|
|
Future<List<String>> getChangedPubSpecs() async {
|
|
return (await getChangedFiles()).where(_isPubspec).toList();
|
|
}
|
|
|
|
/// Get a list of all the changed files.
|
|
Future<List<String>> getChangedFiles() async {
|
|
final String baseSha = await _getBaseSha();
|
|
final io.ProcessResult changedFilesCommand = await baseGitDir
|
|
.runCommand(<String>['diff', '--name-only', baseSha, 'HEAD']);
|
|
print('Determine diff with base sha: $baseSha');
|
|
final String changedFilesStdout = changedFilesCommand.stdout.toString();
|
|
if (changedFilesStdout.isEmpty) {
|
|
return <String>[];
|
|
}
|
|
final List<String> changedFiles = changedFilesStdout.split('\n')
|
|
..removeWhere((String element) => element.isEmpty);
|
|
return changedFiles.toList();
|
|
}
|
|
|
|
/// Get the package version specified in the pubspec file in `pubspecPath` and
|
|
/// at the revision of `gitRef` (defaulting to the base if not provided).
|
|
Future<Version?> getPackageVersion(String pubspecPath,
|
|
{String? gitRef}) async {
|
|
final String ref = gitRef ?? (await _getBaseSha());
|
|
|
|
io.ProcessResult gitShow;
|
|
try {
|
|
gitShow =
|
|
await baseGitDir.runCommand(<String>['show', '$ref:$pubspecPath']);
|
|
} on io.ProcessException {
|
|
return null;
|
|
}
|
|
final String fileContent = gitShow.stdout as String;
|
|
final String? versionString = loadYaml(fileContent)['version'] as String?;
|
|
return versionString == null ? null : Version.parse(versionString);
|
|
}
|
|
|
|
Future<String> _getBaseSha() async {
|
|
if (baseSha != null && baseSha!.isNotEmpty) {
|
|
return baseSha!;
|
|
}
|
|
|
|
io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand(
|
|
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
|
|
throwOnError: false);
|
|
if (baseShaFromMergeBase.stderr != null ||
|
|
baseShaFromMergeBase.stdout == null) {
|
|
baseShaFromMergeBase = await baseGitDir
|
|
.runCommand(<String>['merge-base', 'FETCH_HEAD', 'HEAD']);
|
|
}
|
|
return (baseShaFromMergeBase.stdout as String).trim();
|
|
}
|
|
}
|