| #!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: ./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 = 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 branchNames = Directory(p.join(dir, '.git', 'refs', 'heads')) |
| .listSync() |
| .whereType<File>() |
| .map((f) => p.basename(f.path)) |
| .toSet(); |
| |
| for (var name in ['main', 'master']) { |
| if (branchNames.contains(name)) { |
| return name; |
| } |
| } |
| |
| return 'HEAD'; |
| } |