| #!tools/sdks/dart-sdk/bin/dart | |
| // Copyright (c) 2021, 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. | |
| /// Helps rolling dependency to the newest version available | |
| /// (or a target version). | |
| /// | |
| /// Usage: | |
| /// | |
| /// ```bash | |
| /// ./tools/manage_deps.dart bump <dependency> [--branch <branch>] [--target <ref>] | |
| /// ``` | |
| /// | |
| /// This will: | |
| /// 0. Check that git is clean | |
| /// 1. Create branch `<branch> ?? bump_<dependency>` | |
| /// 2. Update `DEPS` for `<dependency>` | |
| /// 3. Create a commit with `git log` of imported commits in the message. | |
| /// 4. Prompt to create a CL | |
| // @dart = 2.13 | |
| library bump; | |
| import 'dart:io'; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:path/path.dart' as p; | |
| class BumpCommand extends Command<int> { | |
| @override | |
| String get description => ''' | |
| Bump a dependency in DEPS and create a CL | |
| This will: | |
| 0. Check that git is clean | |
| 1. Create branch `<branch> ?? bump_<dependency>` | |
| 2. Update `DEPS` for `<dependency>` | |
| 3. Create a commit with `git log` of imported commits in the message. | |
| 4. Prompt to create a CL | |
| '''; | |
| @override | |
| String get invocation => | |
| './tools/manage_deps.dart bump <path/to/dependency> <options>'; | |
| BumpCommand() { | |
| argParser.addOption( | |
| 'branch', | |
| help: 'The name of the branch where the update is created.', | |
| valueHelp: 'branch-name', | |
| ); | |
| argParser.addOption( | |
| 'target', | |
| help: 'The git ref to update to.', | |
| valueHelp: 'ref', | |
| ); | |
| } | |
| @override | |
| String get name => 'bump'; | |
| @override | |
| Future<int> run() async { | |
| final argResults = this.argResults!; | |
| if (argResults.rest.length != 1) { | |
| usageException('No dependency directory given'); | |
| } | |
| final status = runProcessForLines(['git', 'status', '--porcelain'], | |
| explanation: 'Checking if your git checkout is clean'); | |
| if (status.isNotEmpty) { | |
| print('Note your git checkout is dirty!'); | |
| } | |
| final pkgDir = p.normalize(argResults.rest.first); | |
| if (!Directory(pkgDir).existsSync()) { | |
| usageException('No directory $pkgDir'); | |
| } | |
| final toUpdate = p.split(pkgDir).last; | |
| final branchName = argResults.option('branch') ?? 'bump_$toUpdate'; | |
| final exists = runProcessForExitCode( | |
| ['git', 'rev-parse', '--verify', branchName], | |
| explanation: 'Checking if branch-name exists'); | |
| if (exists == 0) { | |
| print('Branch $branchName already exist - delete it?'); | |
| if (!prompt()) { | |
| print('Ok - exiting'); | |
| exit(-1); | |
| } | |
| runProcessAssumingSuccess( | |
| ['git', 'branch', '-D', branchName], | |
| explanation: 'Deleting existing branch', | |
| ); | |
| } | |
| runProcessAssumingSuccess( | |
| ['git', 'checkout', '-b', branchName], | |
| explanation: 'Creating branch', | |
| ); | |
| final currentRev = runProcessForLines( | |
| ['gclient', 'getdep', '-r', p.join('sdk', pkgDir)], | |
| explanation: 'Finding current revision', | |
| ).first; | |
| final originUrl = runProcessForLines( | |
| ['git', 'config', '--get', 'remote.origin.url'], | |
| workingDirectory: pkgDir, | |
| explanation: 'Finding origin url', | |
| ).first; | |
| runProcessAssumingSuccess( | |
| ['git', 'fetch', 'origin'], | |
| workingDirectory: pkgDir, | |
| explanation: 'Retrieving updates to $toUpdate', | |
| ); | |
| final gitRevParseResult = runProcessForLines([ | |
| 'git', | |
| 'rev-parse', | |
| if (argResults.wasParsed('target')) | |
| argResults.option('target')! | |
| else | |
| 'origin/${defaultBranchTarget(pkgDir)}', | |
| ], workingDirectory: pkgDir, explanation: 'Finding sha-id'); | |
| final target = gitRevParseResult.first; | |
| if (currentRev == target) { | |
| print('Already at $target - nothing to do'); | |
| return -1; | |
| } | |
| runProcessAssumingSuccess( | |
| ['gclient', 'setdep', '-r', '${p.join('sdk', pkgDir)}@$target'], | |
| explanation: 'Updating $toUpdate', | |
| ); | |
| runProcessAssumingSuccess( | |
| ['gclient', 'sync', '-D'], | |
| explanation: 'Syncing your deps', | |
| ); | |
| runProcessAssumingSuccess( | |
| [ | |
| Platform.resolvedExecutable, | |
| 'tools/generate_package_config.dart', | |
| ], | |
| explanation: 'Updating package config', | |
| ); | |
| final gitLogResult = runProcessForLines([ | |
| 'git', | |
| 'log', | |
| '--format=%C(auto) $originUrl/+/%h %s ', | |
| '$currentRev..$target', | |
| ], workingDirectory: pkgDir, explanation: 'Listing new commits'); | |
| // To avoid github notifying issues in the sdk when it sees #issueid | |
| // we remove all '#' characters. | |
| final cleanedGitLogResult = | |
| gitLogResult.map((x) => x.replaceAll('#', '')).join('\n'); | |
| final commitMessage = ''' | |
| Bump $toUpdate to $target | |
| Changes: | |
| ``` | |
| > git log --format="%C(auto) %h %s" ${currentRev.substring(0, 7)}..${target.substring(0, 7)} | |
| $cleanedGitLogResult | |
| ``` | |
| Diff: $originUrl/+/$currentRev..$target/ | |
| '''; | |
| runProcessAssumingSuccess(['git', 'commit', '-am', commitMessage], | |
| explanation: 'Committing'); | |
| print('Consider updating CHANGELOG.md'); | |
| print('Do you want to create a CL?'); | |
| if (prompt()) { | |
| await runProcessInteractively( | |
| ['git', 'cl', 'upload', '-m', commitMessage], | |
| explanation: 'Creating CL', | |
| ); | |
| } | |
| return 0; | |
| } | |
| } | |
| Future<void> main(List<String> args) async { | |
| final runner = CommandRunner<int>( | |
| 'manage_deps.dart', 'helps managing the DEPS file', | |
| usageLineLength: 80) | |
| ..addCommand(BumpCommand()); | |
| try { | |
| exit(await runner.run(args) ?? -1); | |
| } on UsageException catch (e) { | |
| print(e.message); | |
| print(e.usage); | |
| } | |
| } | |
| bool prompt() { | |
| stdout.write('(y/N):'); | |
| final answer = stdin.readLineSync() ?? ''; | |
| return answer.trim().toLowerCase() == 'y'; | |
| } | |
| void printRunningLine( | |
| List<String> cmd, String? explanation, String? workingDirectory) { | |
| stdout.write( | |
| "${explanation ?? 'Running'}: `${cmd.join(' ')}` ${workingDirectory == null ? '' : 'in $workingDirectory'}"); | |
| } | |
| void printSuccessTrailer(ProcessResult result, String? onFailure) { | |
| if (result.exitCode == 0) { | |
| stdout.writeln(' ✓'); | |
| } else { | |
| stdout.writeln(' X'); | |
| stderr.write(result.stdout); | |
| stderr.write(result.stderr); | |
| if (onFailure != null) { | |
| print(onFailure); | |
| } | |
| throw Exception(); | |
| } | |
| } | |
| void runProcessAssumingSuccess(List<String> cmd, | |
| {String? explanation, | |
| String? workingDirectory, | |
| Map<String, String> environment = const {}, | |
| String? onFailure}) { | |
| printRunningLine(cmd, explanation, workingDirectory); | |
| final result = Process.runSync( | |
| cmd[0], | |
| cmd.skip(1).toList(), | |
| workingDirectory: workingDirectory, | |
| environment: environment, | |
| ); | |
| printSuccessTrailer(result, onFailure); | |
| } | |
| List<String> runProcessForLines(List<String> cmd, | |
| {String? explanation, String? workingDirectory, String? onFailure}) { | |
| printRunningLine(cmd, explanation, workingDirectory); | |
| final result = Process.runSync( | |
| cmd[0], | |
| cmd.skip(1).toList(), | |
| workingDirectory: workingDirectory, | |
| environment: {'DEPOT_TOOLS_UPDATE': '0'}, | |
| ); | |
| printSuccessTrailer(result, onFailure); | |
| final output = (result.stdout as String); | |
| return output == '' ? <String>[] : output.split('\n'); | |
| } | |
| Future<void> runProcessInteractively(List<String> cmd, | |
| {String? explanation, String? workingDirectory}) async { | |
| printRunningLine(cmd, explanation, workingDirectory); | |
| stdout.writeln(''); | |
| final process = await Process.start(cmd[0], cmd.skip(1).toList(), | |
| workingDirectory: workingDirectory, mode: ProcessStartMode.inheritStdio); | |
| final exitCode = await process.exitCode; | |
| if (exitCode != 0) { | |
| throw Exception(); | |
| } | |
| } | |
| int runProcessForExitCode(List<String> cmd, | |
| {String? explanation, String? workingDirectory}) { | |
| printRunningLine(cmd, explanation, workingDirectory); | |
| final result = Process.runSync( | |
| cmd[0], | |
| cmd.skip(1).toList(), | |
| workingDirectory: workingDirectory, | |
| ); | |
| stdout.writeln(' => ${result.exitCode}'); | |
| return result.exitCode; | |
| } | |
| String defaultBranchTarget(String dir) { | |
| var result = Process.runSync('git', ['branch', '--list', 'main', 'master'], | |
| workingDirectory: dir); | |
| if (result.exitCode == 0) { | |
| var output = (result.stdout as String).trim(); | |
| var branches = output | |
| .split('\n') | |
| .map((line) => line.replaceAll('*', '').trim()) | |
| .toSet(); | |
| for (var name in ['main', 'master']) { | |
| if (branches.contains(name)) { | |
| return name; | |
| } | |
| } | |
| } | |
| return 'HEAD'; | |
| } |