|  | #!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 = 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'; | 
|  | } |