feat: Give cutler some sane default behavior (#578)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
||||
# avoid_print can be removed now that we have a logger.
|
||||
avoid_print: false
|
||||
|
||||
@@ -5,17 +5,20 @@ import 'package:args/command_runner.dart';
|
||||
import 'package:cutler/commands/commands.dart';
|
||||
import 'package:cutler/config.dart';
|
||||
import 'package:cutler/model.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class Cutler extends CommandRunner<int> {
|
||||
Cutler() : super('cutler', 'A tool for maintaining forks of Flutter.') {
|
||||
addCommand(RebaseCommand());
|
||||
addCommand(PrintVersionsCommand());
|
||||
Cutler({Logger? logger})
|
||||
: _logger = logger ?? Logger(),
|
||||
super('cutler', 'A tool for maintaining forks of Flutter.') {
|
||||
addCommand(RebaseCommand(logger: _logger));
|
||||
addCommand(PrintVersionsCommand(logger: _logger));
|
||||
|
||||
argParser
|
||||
..addFlag('verbose', abbr: 'v')
|
||||
..addOption(
|
||||
'root',
|
||||
defaultsTo: '.',
|
||||
help: 'Directory in which to find checkouts.',
|
||||
)
|
||||
..addOption(
|
||||
@@ -27,24 +30,60 @@ class Cutler extends CommandRunner<int> {
|
||||
..addFlag('update', defaultsTo: true, help: 'Update checkouts.');
|
||||
}
|
||||
|
||||
final Logger _logger;
|
||||
|
||||
Iterable<String> missingDirectories(String rootDir) {
|
||||
return Repo.values.map((repo) => '$rootDir/${repo.path}').where(
|
||||
(path) => !Directory(path).existsSync(),
|
||||
);
|
||||
}
|
||||
|
||||
String fallbackRootDir() {
|
||||
final cutlerBin = p.dirname(Platform.script.path);
|
||||
final cutlerRoot = p.dirname(cutlerBin);
|
||||
final packagesDir = p.dirname(cutlerRoot);
|
||||
final shorebirdDir = p.dirname(packagesDir);
|
||||
final fallbackDirectories = <String>[
|
||||
Directory.current.path,
|
||||
p.dirname(shorebirdDir),
|
||||
// Internal checkouts use a _shorebird wrapper directory.
|
||||
p.dirname(p.dirname(shorebirdDir)),
|
||||
];
|
||||
for (final directory in fallbackDirectories) {
|
||||
if (missingDirectories(directory).isEmpty) {
|
||||
print('Using $directory as checkouts root.');
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
_logger.err('Failed to find a valid checkouts root, tried:\n'
|
||||
'${fallbackDirectories.join('\n')}');
|
||||
return ''; // Returning an invalid directory will cause validation to fail.
|
||||
}
|
||||
|
||||
@override
|
||||
ArgResults parse(Iterable<String> args) {
|
||||
final results = super.parse(args);
|
||||
|
||||
final rootDir = results['root'] as String? ?? fallbackRootDir();
|
||||
|
||||
final missingDirs = missingDirectories(rootDir);
|
||||
if (missingDirs.isNotEmpty) {
|
||||
_logger
|
||||
..err('Could not find a valid checkouts root.')
|
||||
..err('--root must be a directory containing the '
|
||||
'following:\n${Repo.values.map((r) => r.path).join('\n')}')
|
||||
..err('Missing directories:\n${missingDirs.join('\n')}');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
config = Config(
|
||||
checkoutsRoot: expandUser(results['root'] as String),
|
||||
checkoutsRoot: expandUser(rootDir),
|
||||
verbose: results['verbose'] as bool,
|
||||
dryRun: results['dry-run'] as bool,
|
||||
doUpdate: results['update'] as bool,
|
||||
flutterChannel: results['flutter-channel'] as String,
|
||||
);
|
||||
for (final repo in Repo.values) {
|
||||
final path = '${config.checkoutsRoot}/${repo.path}';
|
||||
if (!Directory(path).existsSync()) {
|
||||
throw Exception(
|
||||
'Directory $path does not exist, are you sure --root is correct?',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
|
||||
/// Base class for Cutler subcommands.
|
||||
abstract class CutlerCommand extends Command<int> {
|
||||
/// Constructs a new [CutlerCommand].
|
||||
CutlerCommand({
|
||||
required this.logger,
|
||||
});
|
||||
|
||||
/// The logger to use for this command.
|
||||
final Logger logger;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:cutler/commands/base.dart';
|
||||
import 'package:cutler/git_extensions.dart';
|
||||
import 'package:cutler/model.dart';
|
||||
import 'package:cutler/versions.dart';
|
||||
import 'package:io/io.dart';
|
||||
|
||||
class PrintVersionsCommand extends Command<int> {
|
||||
PrintVersionsCommand();
|
||||
/// Print the versions a given Shorebird release hash depends on.
|
||||
class PrintVersionsCommand extends CutlerCommand {
|
||||
/// Constructs a new [PrintVersionsCommand] with a given [logger].
|
||||
PrintVersionsCommand({required super.logger});
|
||||
@override
|
||||
final name = 'print-versions';
|
||||
@override
|
||||
@@ -14,12 +16,30 @@ class PrintVersionsCommand extends Command<int> {
|
||||
|
||||
@override
|
||||
int run() {
|
||||
final shorebirdHash = argResults!.rest.first;
|
||||
late final String shorebirdHash;
|
||||
if (argResults!.rest.isEmpty) {
|
||||
print('No Shorebird hash provided, using `origin/stable`.');
|
||||
shorebirdHash = 'origin/stable';
|
||||
} else {
|
||||
shorebirdHash = argResults!.rest.first;
|
||||
}
|
||||
|
||||
final shorebirdFlutter = Repo.shorebird
|
||||
.contentsAtPath(shorebirdHash, 'bin/internal/flutter.version');
|
||||
final shorebird = getFlutterVersions(shorebirdFlutter);
|
||||
print('Shorebird $shorebirdHash:');
|
||||
printVersions(shorebird, 2);
|
||||
logger.info('Shorebird $shorebirdHash:');
|
||||
printVersions(shorebird, indent: 2);
|
||||
|
||||
final flutterForkpoint = Repo.flutter.getForkPoint(shorebird.flutter.hash);
|
||||
// This is slightly error-prone in that we're assuming that our engine and
|
||||
// buildroot forks started from the correct commit. But I'm not sure how
|
||||
// to determine the forkpoint otherwise. engine and buildroot don't have
|
||||
// a stable branch, yet they do seem to "branch" for stable releases at the
|
||||
// x.x.0 release.
|
||||
final forkpoints = getFlutterVersions(flutterForkpoint.hash);
|
||||
logger.info('Forkpoints:');
|
||||
printVersions(forkpoints, indent: 2);
|
||||
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:cutler/commands/base.dart';
|
||||
import 'package:cutler/config.dart';
|
||||
import 'package:cutler/git_extensions.dart';
|
||||
import 'package:cutler/model.dart';
|
||||
import 'package:cutler/versions.dart';
|
||||
import 'package:io/io.dart';
|
||||
|
||||
/// Prints the latest commit for a given [branch] in a given [repo].
|
||||
String printLatestForBranch(Repo repo, String branch) {
|
||||
final hash = repo.getLatestCommit(branch);
|
||||
final tags = repo.getTagsFor(hash);
|
||||
@@ -43,8 +44,11 @@ String rebaseRepo(
|
||||
return shorebird[repo].ref;
|
||||
}
|
||||
|
||||
class RebaseCommand extends Command<int> {
|
||||
RebaseCommand();
|
||||
/// Print the commands needed to rebase our repos onto the given Flutter
|
||||
/// revision.
|
||||
class RebaseCommand extends CutlerCommand {
|
||||
/// Constructs a new [RebaseCommand] with a given [logger].
|
||||
RebaseCommand({required super.logger});
|
||||
@override
|
||||
final name = 'rebase';
|
||||
@override
|
||||
@@ -70,7 +74,7 @@ class RebaseCommand extends Command<int> {
|
||||
.contentsAtPath(shorebirdStable, 'bin/internal/flutter.version');
|
||||
final shorebird = getFlutterVersions(shorebirdFlutter);
|
||||
print('Shorebird stable:');
|
||||
printVersions(shorebird, 2);
|
||||
printVersions(shorebird, indent: 2);
|
||||
|
||||
final flutterForkpoint = Repo.flutter.getForkPoint(shorebird.flutter.hash);
|
||||
// This is slightly error-prone in that we're assuming that our engine and
|
||||
@@ -80,7 +84,7 @@ class RebaseCommand extends Command<int> {
|
||||
// x.x.0 release.
|
||||
final forkpoints = getFlutterVersions(flutterForkpoint.hash);
|
||||
print('Forkpoints:');
|
||||
printVersions(forkpoints, 2);
|
||||
printVersions(forkpoints, indent: 2);
|
||||
|
||||
// Figure out the latest version of Flutter.
|
||||
final upstreamFlutter =
|
||||
@@ -88,7 +92,7 @@ class RebaseCommand extends Command<int> {
|
||||
// Figure out what versions that Flutter depends on.
|
||||
final upstream = getFlutterVersions(upstreamFlutter);
|
||||
print('Upstream ${config.flutterChannel}:');
|
||||
printVersions(upstream, 2);
|
||||
printVersions(upstream, indent: 2);
|
||||
|
||||
Version doRebase(Repo repo) {
|
||||
final newHash = rebaseRepo(
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:io';
|
||||
|
||||
// https://github.com/dart-lang/sdk/issues/18466
|
||||
// https://github.com/dart-lang/path/issues/117#issuecomment-1034313012
|
||||
/// Expands a path that may contain a user directory (`~`). If [env] is
|
||||
/// provided, it will be used instead of [Platform.environment].
|
||||
String expandUser(String path, {Map<String, String>? env}) {
|
||||
// This is not "well written", but eventually I guess we should
|
||||
// write one and publish it if Dart doesn't add an equivalent method.
|
||||
@@ -20,7 +22,9 @@ String expandUser(String path, {Map<String, String>? env}) {
|
||||
}
|
||||
|
||||
// Config is basically just our typed ArgResults held as a global.
|
||||
/// Global configuration object for Cutler.
|
||||
class Config {
|
||||
/// Constructs a new [Config].
|
||||
Config({
|
||||
required this.checkoutsRoot,
|
||||
required this.verbose,
|
||||
@@ -28,13 +32,25 @@ class Config {
|
||||
required this.doUpdate,
|
||||
required this.flutterChannel,
|
||||
});
|
||||
|
||||
/// The root directory where checkouts can be found.
|
||||
final String checkoutsRoot;
|
||||
|
||||
/// Whether to print verbose output.
|
||||
final bool verbose;
|
||||
|
||||
/// Whether to perform a dry run.
|
||||
final bool dryRun;
|
||||
|
||||
/// Whether to update checkouts.
|
||||
final bool doUpdate;
|
||||
|
||||
/// The Flutter channel to use.
|
||||
final String flutterChannel;
|
||||
|
||||
/// The name of the release branch for Shorebird.
|
||||
final String shorebirdReleaseBranch = 'origin/stable';
|
||||
}
|
||||
|
||||
/// The global configuration object for Cutler.
|
||||
late final Config config;
|
||||
|
||||
@@ -35,6 +35,7 @@ String runCommand(
|
||||
return result.stdout.toString().trim();
|
||||
}
|
||||
|
||||
/// Function to print the command that would be run, but not actually run it.
|
||||
void dryRunCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
@@ -43,7 +44,9 @@ void dryRunCommand(
|
||||
print("$executable ${arguments.join(' ')}");
|
||||
}
|
||||
|
||||
/// Extension methods for [Repo] to do actual `git` actions.
|
||||
extension RepoCommands on Repo {
|
||||
/// Returns a [Version] for the given [hash].
|
||||
Version versionFrom(String hash, {bool lookupTags = true}) {
|
||||
return Version(
|
||||
hash: hash,
|
||||
@@ -52,8 +55,10 @@ extension RepoCommands on Repo {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the working directory for this repo.
|
||||
String get workingDirectory => '${config.checkoutsRoot}/$path';
|
||||
|
||||
/// Returns the latest commit for a given [branch] in this repo.
|
||||
String getLatestCommit(String branch) {
|
||||
return runCommand(
|
||||
'git',
|
||||
@@ -62,6 +67,7 @@ extension RepoCommands on Repo {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the tags for a given [commit] in this repo.
|
||||
List<String> getTagsFor(String commit) {
|
||||
final output = runCommand(
|
||||
'git',
|
||||
@@ -74,6 +80,7 @@ extension RepoCommands on Repo {
|
||||
return output.split('\n');
|
||||
}
|
||||
|
||||
/// Returns the fork point as a [Version] for this repo given a [forkBranch].
|
||||
Version getForkPoint(String forkBranch) {
|
||||
final hash = runCommand(
|
||||
'git',
|
||||
@@ -83,6 +90,8 @@ extension RepoCommands on Repo {
|
||||
return versionFrom(hash);
|
||||
}
|
||||
|
||||
/// Returns the contents of a file at a given [path] in this repo at a given
|
||||
/// [commit].
|
||||
String contentsAtPath(String commit, String path) {
|
||||
return runCommand(
|
||||
'git',
|
||||
@@ -91,10 +100,12 @@ extension RepoCommands on Repo {
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes [contents] to a file at a given [path] in this repo.
|
||||
void writeFile(String path, String contents) {
|
||||
File(path).writeAsStringSync(contents);
|
||||
}
|
||||
|
||||
/// Commits to this repo with a given [message].
|
||||
void commit(String message) {
|
||||
runCommand(
|
||||
'git',
|
||||
@@ -103,6 +114,7 @@ extension RepoCommands on Repo {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a [Version] object representing the current HEAD of this repo.
|
||||
Version localHead() {
|
||||
return versionFrom(
|
||||
runCommand(
|
||||
@@ -114,7 +126,10 @@ extension RepoCommands on Repo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods for [Version] to do actual `git` actions.
|
||||
extension VersionCommands on Version {
|
||||
/// Returns the contents of a file at a given [path] in this repo at this
|
||||
/// version.
|
||||
String contentsAtPath(String path) {
|
||||
return repo.contentsAtPath(hash, path);
|
||||
}
|
||||
|
||||
@@ -2,24 +2,31 @@ import 'package:meta/meta.dart';
|
||||
|
||||
/// Configuration information for each of our repos.
|
||||
enum Repo {
|
||||
/// Repo configuration representing the Shorebird repo.
|
||||
shorebird(
|
||||
name: 'shorebird',
|
||||
path: '_shorebird/shorebird',
|
||||
url: 'https://github.com/shorebirdtech/shorebird.git',
|
||||
upstreamBranch: 'origin/main',
|
||||
),
|
||||
|
||||
/// Repo configuration representing the Flutter repo.
|
||||
flutter(
|
||||
name: 'flutter',
|
||||
path: 'flutter',
|
||||
url: 'https://github.com/shorebirdtech/flutter.git',
|
||||
upstreamBranch: 'upstream/stable',
|
||||
),
|
||||
|
||||
/// Repo configuration representing the engine repo.
|
||||
engine(
|
||||
name: 'engine',
|
||||
path: 'engine/src/flutter',
|
||||
url: 'https://github.com/shorebirdtech/engine.git',
|
||||
upstreamBranch: 'upstream/master',
|
||||
),
|
||||
|
||||
/// Repo configuration representing the buildroot repo.
|
||||
buildroot(
|
||||
name: 'buildroot',
|
||||
path: 'engine/src',
|
||||
@@ -34,35 +41,57 @@ enum Repo {
|
||||
required this.upstreamBranch,
|
||||
});
|
||||
|
||||
/// Returns the name (e.g. 'engine') of the repo.
|
||||
final String name;
|
||||
|
||||
/// Returns the path (e.g. 'engine/src/flutter') of the repo.
|
||||
final String path;
|
||||
|
||||
/// Returns the URL the repo is cloned from.
|
||||
final String url;
|
||||
|
||||
/// Returns the name of the upstream branch.
|
||||
final String upstreamBranch;
|
||||
}
|
||||
|
||||
/// Paths to version files in each repo.
|
||||
enum Paths {
|
||||
/// Path to the engine DEPS file in the engine repo.
|
||||
engineDEPS('DEPS'),
|
||||
|
||||
/// Path to the flutter engine version file in the flutter repo.
|
||||
flutterEngineVersion('bin/internal/engine.version'),
|
||||
|
||||
/// Path to the flutter version file in the shorebird repo.
|
||||
shorebirdFlutterVersion('bin/internal/flutter.version');
|
||||
|
||||
const Paths(this.path);
|
||||
|
||||
/// Returns the path (e.g. 'DEPS') of the file.
|
||||
final String path;
|
||||
}
|
||||
|
||||
/// An object to pair a [hash] with a [repo].
|
||||
@immutable
|
||||
class Version {
|
||||
/// Constructs a new [Version] object for a given [hash] and [repo]
|
||||
/// with optionally provided [aliases] for the hash (typically tag names).
|
||||
const Version({
|
||||
required this.hash,
|
||||
required this.repo,
|
||||
this.aliases = const [],
|
||||
});
|
||||
|
||||
/// The hash of the version.
|
||||
final String hash;
|
||||
|
||||
/// Aliaes for the hash (typically tag names).
|
||||
final List<String> aliases;
|
||||
|
||||
/// The repo the version is from.
|
||||
final Repo repo;
|
||||
|
||||
/// Returns the first alias for the hash, or the hash if there are no aliases.
|
||||
String get ref => aliases.isEmpty ? hash : aliases.first;
|
||||
|
||||
@override
|
||||
@@ -85,21 +114,31 @@ class Version {
|
||||
|
||||
/// An object to hold a set of versions that make up a Flutter release.
|
||||
class VersionSet {
|
||||
/// Constructs a new [VersionSet] with a given [engine], [flutter], and
|
||||
/// [buildroot] version.
|
||||
const VersionSet({
|
||||
required this.engine,
|
||||
required this.flutter,
|
||||
required this.buildroot,
|
||||
});
|
||||
|
||||
/// The engine version.
|
||||
final Version engine;
|
||||
|
||||
/// The flutter version.
|
||||
final Version flutter;
|
||||
|
||||
/// The buildroot version.
|
||||
final Version buildroot;
|
||||
|
||||
/// Returns the version for a given [repo].
|
||||
Version operator [](Repo repo) => {
|
||||
Repo.engine: engine,
|
||||
Repo.flutter: flutter,
|
||||
Repo.buildroot: buildroot,
|
||||
}[repo]!;
|
||||
|
||||
/// Copies the VersionSet replacing any provided values.
|
||||
VersionSet copyWith({Version? engine, Version? flutter, Version? buildroot}) {
|
||||
return VersionSet(
|
||||
engine: engine ?? this.engine,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'package:cutler/git_extensions.dart';
|
||||
import 'package:cutler/model.dart';
|
||||
|
||||
void printVersions(VersionSet versions, int indent) {
|
||||
/// Print VersionSet [versions] to stdout at a given [indent] level.
|
||||
void printVersions(VersionSet versions, {int indent = 0}) {
|
||||
print("${' ' * indent}flutter ${versions.flutter}");
|
||||
print("${' ' * indent}engine ${versions.engine}");
|
||||
print("${' ' * indent}buildroot ${versions.buildroot}");
|
||||
}
|
||||
|
||||
/// Returns a [VersionSet] for Flutter for a given [flutterHash].
|
||||
/// e.g. `flutterHash` might be `origin/stable` or `v1.22.0-12.1.pre`.
|
||||
/// and this would return the set of versions (engine and buildroot) that
|
||||
/// Flutter depends on for that release.
|
||||
VersionSet getFlutterVersions(String flutterHash) {
|
||||
final engineHash = Repo.flutter
|
||||
.contentsAtPath(flutterHash, 'bin/internal/engine.version')
|
||||
@@ -21,6 +26,7 @@ VersionSet getFlutterVersions(String flutterHash) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses the given DEPS file contents and returns the buildroot version.
|
||||
String parseBuildRoot(String depsContents) {
|
||||
final lines = depsContents.split('\n');
|
||||
// Example:
|
||||
|
||||
@@ -137,6 +137,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
mason_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mason_logger
|
||||
sha256: "389dda35ee44c8664490749b204de1ee2dd91fbe9a725c2798f3232010dc53de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -13,5 +13,6 @@ dev_dependencies:
|
||||
dependencies:
|
||||
args: ^2.4.0
|
||||
io: ^1.0.4
|
||||
mason_logger: ^0.2.6
|
||||
meta: ^1.9.1
|
||||
path: ^1.8.3
|
||||
|
||||
Reference in New Issue
Block a user