diff --git a/README.md b/README.md index 1adf3184..5c6102b1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This repository is home to general Dart Ecosystem tools and packages. | [firehose](pkgs/firehose/) | A tool to automate publishing of Pub packages from GitHub actions. | [![pub package](https://img.shields.io/pub/v/firehose.svg)](https://pub.dev/packages/firehose) | | [repo_manage](pkgs/repo_manage/) | Miscellaneous issue, repo, and PR query tools. | | | [sdk_triage_bot](pkgs/sdk_triage_bot/) | A triage automation tool for dart-lang/sdk issues. | | +| [trebuchet](pkgs/trebuchet/) | A tool for moving existing packages into monorepos. | | ## Publishing automation diff --git a/pkgs/trebuchet/README.md b/pkgs/trebuchet/README.md new file mode 100644 index 00000000..21f94013 --- /dev/null +++ b/pkgs/trebuchet/README.md @@ -0,0 +1,16 @@ +## What's this? + +This is a tool to move existing packages into monorepos. + +## Running this tool + +```bash +dart run bin/trebuchet.dart \ +--input-name coverage \ +--branch-name master \ +--input-path ~/projects/coverage/ \ +--target-path ~/projects/tools/ \ +--git-filter-repo ~/tools/git-filter-repo +``` + +This basically executes the instructions at https://github.com/dart-lang/ecosystem/wiki/Merging-existing-repos-into-a-monorepo \ No newline at end of file diff --git a/pkgs/trebuchet/analysis_options.yaml b/pkgs/trebuchet/analysis_options.yaml new file mode 100644 index 00000000..df4c571b --- /dev/null +++ b/pkgs/trebuchet/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +linter: + rules: + - prefer_final_locals diff --git a/pkgs/trebuchet/bin/trebuchet.dart b/pkgs/trebuchet/bin/trebuchet.dart new file mode 100644 index 00000000..9286cc23 --- /dev/null +++ b/pkgs/trebuchet/bin/trebuchet.dart @@ -0,0 +1,219 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. 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:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; + +Future main(List arguments) async { + final argParser = ArgParser() + ..addOption( + 'input-name', + help: 'Name of the package which should be transferred to a mono-repo', + ) + ..addOption( + 'input-path', + help: 'Path to the package which should be transferred to a mono-repo', + ) + ..addOption( + 'target-path', + help: 'Path to the mono-repo', + ) + ..addOption( + 'branch-name', + help: 'The name of the main branch on the input repo', + defaultsTo: 'main', + ) + ..addOption( + 'git-filter-repo', + help: 'Path to the git-filter-repo tool', + ) + ..addFlag( + 'dry-run', + help: 'Do not actually execute any of the steps', + defaultsTo: false, + ) + ..addFlag( + 'help', + abbr: 'h', + help: 'Prints usage info', + negatable: false, + ); + + String? input; + String? inputPath; + String? targetPath; + String? branchName; + String? gitFilterRepo; + bool dryRun; + try { + final parsed = argParser.parse(arguments); + if (parsed.flag('help')) { + print(argParser.usage); + exit(0); + } + + input = parsed.option('input-name')!; + inputPath = parsed.option('input-path')!; + targetPath = parsed.option('target-path')!; + branchName = parsed.option('branch-name')!; + gitFilterRepo = parsed.option('git-filter-repo')!; + dryRun = parsed.flag('dry-run'); + } catch (e) { + print(e); + print(''); + print(argParser.usage); + exit(1); + } + + final trebuchet = Trebuchet( + input: input, + inputPath: inputPath, + targetPath: targetPath, + branchName: branchName, + gitFilterRepo: gitFilterRepo, + dryRun: dryRun, + ); + + await trebuchet.hurl(); +} + +class Trebuchet { + final String input; + final String inputPath; + final String targetPath; + final String branchName; + final String gitFilterRepo; + final bool dryRun; + + Trebuchet({ + required this.input, + required this.inputPath, + required this.targetPath, + required this.branchName, + required this.gitFilterRepo, + required this.dryRun, + }); + + Future hurl() async { + print('Check existence of python3 on path'); + await runProcess( + 'python3', + ['--version'], + inTarget: false, + ); + + print('Start moving package'); + + print('Rename to `pkgs/`'); + await filterRepo(['--path-rename', ':pkgs/$input/']); + + print('Prefix tags'); + await filterRepo(['--tag-rename', ':$input-']); + + print('Replace issue references in commit messages'); + await inTempDir((tempDirectory) async { + final regexFile = File(p.join(tempDirectory.path, 'expressions.txt')); + await regexFile.create(); + await regexFile.writeAsString('regex:#(\\d)==>dart-lang/$input#\\1'); + await filterRepo(['--replace-message', regexFile.path]); + }); + + print('Create branch at target'); + await runProcess('git', ['checkout', '-b', 'merge-$input-package']); + + print('Add a remote for the local clone of the moving package'); + await runProcess( + 'git', + ['remote', 'add', '${input}_package', inputPath], + ); + await runProcess('git', ['fetch', '${input}_package']); + + print('Merge branch into monorepo'); + await runProcess( + 'git', + [ + 'merge', + '--allow-unrelated-histories', + '${input}_package/$branchName', + '-m', + 'Merge package:$input into shared tool repository' + ], + ); + + final shouldPush = getInput('Push to remote? (y/N)'); + + if (shouldPush) { + print('Push to remote'); + await runProcess( + 'git', + ['push', '--set-upstream', 'origin', 'merge-$input-package'], + ); + } + + print('DONE!'); + print(''' +Steps left to do: + +- Move and fix workflow files +${shouldPush ? '' : '- Run `git push --set-upstream origin merge-$input-package` in the monorepo directory'} +- Disable squash-only in GitHub settings, and merge with a fast forward merge to the main branch, enable squash-only in GitHub settings. +- Push tags to github using `git tag --list '$input*' | xargs git push origin` +- Follow up with a PR adding links to the top-level readme table. +- Add a commit to https://github.com/dart-lang/$input/ with it's readme pointing to the monorepo. +- Update the auto-publishing settings on pub.dev/packages/$input. +- Archive https://github.com/dart-lang/$input/. +'''); + } + + bool getInput(String question) { + print(question); + final line = stdin.readLineSync()?.toLowerCase(); + return line == 'y' || line == 'yes'; + } + + Future runProcess( + String executable, + List arguments, { + bool inTarget = true, + }) async { + final workingDirectory = inTarget ? targetPath : inputPath; + print('----------'); + print('Running `$executable $arguments` in $workingDirectory'); + if (!dryRun) { + final processResult = await Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + ); + print('stdout:'); + print(processResult.stdout); + if ((processResult.stderr as String).isNotEmpty) { + print('stderr:'); + print(processResult.stderr); + } + if (processResult.exitCode != 0) { + throw ProcessException(executable, arguments); + } + } else { + print('Not running, as --dry-run is set.'); + } + print('=========='); + } + + Future filterRepo(List args) async { + await runProcess( + 'python3', + [p.relative(gitFilterRepo, from: inputPath), ...args], + inTarget: false, + ); + } +} + +Future inTempDir(Future Function(Directory temp) f) async { + final tempDirectory = await Directory.systemTemp.createTemp(); + await f(tempDirectory); + await tempDirectory.delete(recursive: true); +} diff --git a/pkgs/trebuchet/pubspec.yaml b/pkgs/trebuchet/pubspec.yaml new file mode 100644 index 00000000..73a9620f --- /dev/null +++ b/pkgs/trebuchet/pubspec.yaml @@ -0,0 +1,16 @@ +name: trebuchet +description: A tool for hurling packages into monorepos. + +publish_to: none + +environment: + sdk: ^3.3.0 + +dependencies: + args: ^2.5.0 + path: ^1.9.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.2.0 + lints: ^4.0.0 + test: ^1.24.0