#!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 [--branch ] [--target ] /// /// This will: /// 0. Check that git is clean /// 1. Create branch ` ?? bump_` /// 2. Update `DEPS` for `` /// 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 { @override String get description => ''' Bump a dependency in DEPS and create a CL This will: 0. Check that git is clean 1. Create branch ` ?? bump_` 2. Update `DEPS` for `` 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 '; 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 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['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['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 main(List args) async { final runner = CommandRunner( '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 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 cmd, {String? explanation, String? workingDirectory, Map 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 runProcessForLines(List 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 == '' ? [] : output.split('\n'); } Future runProcessInteractively(List 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 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() .map((f) => p.basename(f.path)) .toSet(); for (var name in ['main', 'master']) { if (branchNames.contains(name)) { return name; } } return 'HEAD'; }