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