|
| 1 | +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'dart:io'; |
| 6 | + |
| 7 | +import 'package:args/args.dart'; |
| 8 | +import 'package:path/path.dart' as p; |
| 9 | + |
| 10 | +Future<void> main(List<String> arguments) async { |
| 11 | + final argParser = ArgParser() |
| 12 | + ..addOption( |
| 13 | + 'input-name', |
| 14 | + help: 'Name of the package which should be transferred to a mono-repo', |
| 15 | + ) |
| 16 | + ..addOption( |
| 17 | + 'input-path', |
| 18 | + help: 'Path to the package which should be transferred to a mono-repo', |
| 19 | + ) |
| 20 | + ..addOption( |
| 21 | + 'target-path', |
| 22 | + help: 'Path to the mono-repo', |
| 23 | + ) |
| 24 | + ..addOption( |
| 25 | + 'branch-name', |
| 26 | + help: 'The name of the main branch on the input repo', |
| 27 | + defaultsTo: 'main', |
| 28 | + ) |
| 29 | + ..addOption( |
| 30 | + 'git-filter-repo', |
| 31 | + help: 'Path to the git-filter-repo tool', |
| 32 | + ) |
| 33 | + ..addFlag( |
| 34 | + 'dry-run', |
| 35 | + help: 'Do not actually execute any of the steps', |
| 36 | + defaultsTo: false, |
| 37 | + ) |
| 38 | + ..addFlag( |
| 39 | + 'help', |
| 40 | + abbr: 'h', |
| 41 | + help: 'Prints usage info', |
| 42 | + negatable: false, |
| 43 | + ); |
| 44 | + |
| 45 | + String? input; |
| 46 | + String? inputPath; |
| 47 | + String? targetPath; |
| 48 | + String? branchName; |
| 49 | + String? gitFilterRepo; |
| 50 | + bool dryRun; |
| 51 | + try { |
| 52 | + final parsed = argParser.parse(arguments); |
| 53 | + if (parsed.flag('help')) { |
| 54 | + print(argParser.usage); |
| 55 | + exit(0); |
| 56 | + } |
| 57 | + |
| 58 | + input = parsed.option('input-name')!; |
| 59 | + inputPath = parsed.option('input-path')!; |
| 60 | + targetPath = parsed.option('target-path')!; |
| 61 | + branchName = parsed.option('branch-name')!; |
| 62 | + gitFilterRepo = parsed.option('git-filter-repo')!; |
| 63 | + dryRun = parsed.flag('dry-run'); |
| 64 | + } catch (e) { |
| 65 | + print(e); |
| 66 | + print(''); |
| 67 | + print(argParser.usage); |
| 68 | + exit(1); |
| 69 | + } |
| 70 | + |
| 71 | + final trebuchet = Trebuchet( |
| 72 | + input: input, |
| 73 | + inputPath: inputPath, |
| 74 | + targetPath: targetPath, |
| 75 | + branchName: branchName, |
| 76 | + gitFilterRepo: gitFilterRepo, |
| 77 | + dryRun: dryRun, |
| 78 | + ); |
| 79 | + |
| 80 | + await trebuchet.hurl(); |
| 81 | +} |
| 82 | + |
| 83 | +class Trebuchet { |
| 84 | + final String input; |
| 85 | + final String inputPath; |
| 86 | + final String targetPath; |
| 87 | + final String branchName; |
| 88 | + final String gitFilterRepo; |
| 89 | + final bool dryRun; |
| 90 | + |
| 91 | + Trebuchet({ |
| 92 | + required this.input, |
| 93 | + required this.inputPath, |
| 94 | + required this.targetPath, |
| 95 | + required this.branchName, |
| 96 | + required this.gitFilterRepo, |
| 97 | + required this.dryRun, |
| 98 | + }); |
| 99 | + |
| 100 | + Future<void> hurl() async { |
| 101 | + print('Check existence of python3 on path'); |
| 102 | + await runProcess( |
| 103 | + 'python3', |
| 104 | + ['--version'], |
| 105 | + inTarget: false, |
| 106 | + ); |
| 107 | + |
| 108 | + print('Start moving package'); |
| 109 | + |
| 110 | + print('Rename to `pkgs/`'); |
| 111 | + await filterRepo(['--path-rename', ':pkgs/$input/']); |
| 112 | + |
| 113 | + print('Prefix tags'); |
| 114 | + await filterRepo(['--tag-rename', ':$input-']); |
| 115 | + |
| 116 | + print('Replace issue references in commit messages'); |
| 117 | + await inTempDir((tempDirectory) async { |
| 118 | + final regexFile = File(p.join(tempDirectory.path, 'expressions.txt')); |
| 119 | + await regexFile.create(); |
| 120 | + await regexFile.writeAsString('regex:#(\\d)==>dart-lang/$input#\\1'); |
| 121 | + await filterRepo(['--replace-message', regexFile.path]); |
| 122 | + }); |
| 123 | + |
| 124 | + print('Create branch at target'); |
| 125 | + await runProcess('git', ['checkout', '-b', 'merge-$input-package']); |
| 126 | + |
| 127 | + print('Add a remote for the local clone of the moving package'); |
| 128 | + await runProcess( |
| 129 | + 'git', |
| 130 | + ['remote', 'add', '${input}_package', inputPath], |
| 131 | + ); |
| 132 | + await runProcess('git', ['fetch', '${input}_package']); |
| 133 | + |
| 134 | + print('Merge branch into monorepo'); |
| 135 | + await runProcess( |
| 136 | + 'git', |
| 137 | + [ |
| 138 | + 'merge', |
| 139 | + '--allow-unrelated-histories', |
| 140 | + '${input}_package/$branchName', |
| 141 | + '-m', |
| 142 | + 'Merge package:$input into shared tool repository' |
| 143 | + ], |
| 144 | + ); |
| 145 | + |
| 146 | + final shouldPush = getInput('Push to remote? (y/N)'); |
| 147 | + |
| 148 | + if (shouldPush) { |
| 149 | + print('Push to remote'); |
| 150 | + await runProcess( |
| 151 | + 'git', |
| 152 | + ['push', '--set-upstream', 'origin', 'merge-$input-package'], |
| 153 | + ); |
| 154 | + } |
| 155 | + |
| 156 | + print('DONE!'); |
| 157 | + print(''' |
| 158 | +Steps left to do: |
| 159 | +
|
| 160 | +- Move and fix workflow files |
| 161 | +${shouldPush ? '' : '- Run `git push --set-upstream origin merge-$input-package` in the monorepo directory'} |
| 162 | +- Disable squash-only in GitHub settings, and merge with a fast forward merge to the main branch, enable squash-only in GitHub settings. |
| 163 | +- Push tags to github using `git tag --list '$input*' | xargs git push origin` |
| 164 | +- Follow up with a PR adding links to the top-level readme table. |
| 165 | +- Add a commit to https://github.com/dart-lang/$input/ with it's readme pointing to the monorepo. |
| 166 | +- Update the auto-publishing settings on pub.dev/packages/$input. |
| 167 | +- Archive https://github.com/dart-lang/$input/. |
| 168 | +'''); |
| 169 | + } |
| 170 | + |
| 171 | + bool getInput(String question) { |
| 172 | + print(question); |
| 173 | + final line = stdin.readLineSync()?.toLowerCase(); |
| 174 | + return line == 'y' || line == 'yes'; |
| 175 | + } |
| 176 | + |
| 177 | + Future<void> runProcess( |
| 178 | + String executable, |
| 179 | + List<String> arguments, { |
| 180 | + bool inTarget = true, |
| 181 | + }) async { |
| 182 | + final workingDirectory = inTarget ? targetPath : inputPath; |
| 183 | + print('----------'); |
| 184 | + print('Running `$executable $arguments` in $workingDirectory'); |
| 185 | + if (!dryRun) { |
| 186 | + final processResult = await Process.run( |
| 187 | + executable, |
| 188 | + arguments, |
| 189 | + workingDirectory: workingDirectory, |
| 190 | + ); |
| 191 | + print('stdout:'); |
| 192 | + print(processResult.stdout); |
| 193 | + if ((processResult.stderr as String).isNotEmpty) { |
| 194 | + print('stderr:'); |
| 195 | + print(processResult.stderr); |
| 196 | + } |
| 197 | + if (processResult.exitCode != 0) { |
| 198 | + throw ProcessException(executable, arguments); |
| 199 | + } |
| 200 | + } else { |
| 201 | + print('Not running, as --dry-run is set.'); |
| 202 | + } |
| 203 | + print('=========='); |
| 204 | + } |
| 205 | + |
| 206 | + Future<void> filterRepo(List<String> args) async { |
| 207 | + await runProcess( |
| 208 | + 'python3', |
| 209 | + [p.relative(gitFilterRepo, from: inputPath), ...args], |
| 210 | + inTarget: false, |
| 211 | + ); |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +Future<void> inTempDir(Future<void> Function(Directory temp) f) async { |
| 216 | + final tempDirectory = await Directory.systemTemp.createTemp(); |
| 217 | + await f(tempDirectory); |
| 218 | + await tempDirectory.delete(recursive: true); |
| 219 | +} |
0 commit comments