|  | // Copyright (c) 2022, 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. | 
|  |  | 
|  | // Script to automatically update revisions in the DEPS file. | 
|  | // | 
|  | // Anyone can run this, and is welcome to. | 
|  |  | 
|  | import 'dart:convert'; | 
|  | import 'dart:io'; | 
|  |  | 
|  | import 'package:cli_util/cli_logging.dart'; | 
|  | import 'package:path/path.dart' as path; | 
|  | import 'package:pool/pool.dart' as pool; | 
|  |  | 
|  | /// The following set of packages should be individually reviewed. | 
|  | /// | 
|  | /// Generally, they are from repos that are not Dart team owned, and we want to | 
|  | /// ensure that we consistently review all changes from those repos. | 
|  | const Set<String> individuallyReviewedPackages = { | 
|  | 'tar', | 
|  | }; | 
|  |  | 
|  | void main(List<String> args) async { | 
|  | // Validate we're running from the repo root. | 
|  | if (!File('README.dart-sdk').existsSync() || !File('DEPS').existsSync()) { | 
|  | stderr.writeln('Please run this script from the root of the SDK repo.'); | 
|  | exit(1); | 
|  | } | 
|  |  | 
|  | final gclient = GClientHelper(); | 
|  |  | 
|  | final deps = await gclient.getPackageDependencies(); | 
|  | print('${deps.length} package dependencies found.'); | 
|  |  | 
|  | // Remove pinned deps. | 
|  | final pinnedDeps = calculatePinnedDeps(); | 
|  | deps.removeWhere((dep) => pinnedDeps.contains(dep.name)); | 
|  |  | 
|  | print('Not attempting to move forward the revisions for: ' | 
|  | '${pinnedDeps.toList().join(', ')}.'); | 
|  | print(''); | 
|  |  | 
|  | deps.sort((a, b) => a.name.compareTo(b.name)); | 
|  |  | 
|  | final gitPool = pool.Pool(10); | 
|  |  | 
|  | final revDepsToCommits = Map.fromEntries( | 
|  | (await Future.wait( | 
|  | deps.map((dep) { | 
|  | return gitPool.withResource(() async { | 
|  | final git = GitHelper(dep.relativePath); | 
|  | await git.fetch(); | 
|  | var commit = await git.findLatestUnsyncedCommit(); | 
|  | return MapEntry(dep, commit); | 
|  | }); | 
|  | }), | 
|  | )) | 
|  | .where((entry) { | 
|  | final commit = entry.value; | 
|  | return commit.isNotEmpty; | 
|  | }), | 
|  | ); | 
|  |  | 
|  | if (revDepsToCommits.isEmpty) { | 
|  | print('No new revisions.'); | 
|  | return; | 
|  | } | 
|  |  | 
|  | final separateReviewDeps = revDepsToCommits.keys | 
|  | .where((dep) => individuallyReviewedPackages.contains(dep.name)) | 
|  | .toList(); | 
|  | revDepsToCommits | 
|  | .removeWhere((dep, _) => individuallyReviewedPackages.contains(dep.name)); | 
|  |  | 
|  | final depsToRevNames = revDepsToCommits.keys.map((e) => e.name).join(', '); | 
|  |  | 
|  | print('Move moving forward revisions for: $depsToRevNames.'); | 
|  | if (separateReviewDeps.isNotEmpty) { | 
|  | print('(additional, individually reviewed updates are also available for: ' | 
|  | '${separateReviewDeps.map((dep) => dep.name).join(', ')})'); | 
|  | } | 
|  | print(''); | 
|  | print('Commit message:'); | 
|  | print(''); | 
|  | print('[deps] rev $depsToRevNames'); | 
|  | print(''); | 
|  | print('Revisions updated by `dart tools/rev_sdk_deps.dart`.'); | 
|  | print(''); | 
|  |  | 
|  | for (final MapEntry(key: dep, value: commit) in revDepsToCommits.entries) { | 
|  | final git = GitHelper(dep.relativePath); | 
|  |  | 
|  | final gitLog = await git.calculateUnsyncedCommits(); | 
|  | final currentHash = await gclient.getHash(dep); | 
|  |  | 
|  | final gitHubRepo = dep.gitHubRepoIdentifier; | 
|  |  | 
|  | // Construct and print out the GitHub diff URL. | 
|  | print('${dep.name} (${dep.gitHubDiffUrl(currentHash, commit)}):'); | 
|  |  | 
|  | /// Qualify or wrap the GitHub issue references within [commitMessage]. | 
|  | String replaceHashReferences(String commitMessage) => commitMessage | 
|  | .replaceAllMapped( | 
|  | _mergeCommitPullRequestReference, | 
|  | (m) => '($gitHubRepo#${m[1]})', | 
|  | ) | 
|  | .replaceAllMapped(_issueHashReference, (m) => '`${m[0]}`'); | 
|  |  | 
|  | // Format and print out the message header of each new commit. | 
|  | final newCommitHeaders = [ | 
|  | for (final commitHeader in gitLog.split('\n')) | 
|  | '  ${replaceHashReferences(commitHeader)}', | 
|  | ]; | 
|  | print(newCommitHeaders.join('\n').trimRight()); | 
|  |  | 
|  | // Update the DEPS file. | 
|  | await gclient.setHash(dep, commit); | 
|  |  | 
|  | print(''); | 
|  | } | 
|  |  | 
|  | if (separateReviewDeps.isNotEmpty) { | 
|  | final boldText = Ansi(true) | 
|  | .emphasized('Note: updates are also available for additional packages'); | 
|  | print('$boldText; these require individual review.\nPlease ensure that the ' | 
|  | 'review for these changes is thorough. To roll them:'); | 
|  | print(''); | 
|  | for (var dep in separateReviewDeps) { | 
|  | print('${dep.name} from ${dep.url}:'); | 
|  | print('  dart tools/manage_deps.dart bump third_party/pkg/${dep.name}'); | 
|  | print(''); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /// A regex that matches the final PR reference of a merge commit header. | 
|  | /// | 
|  | /// Allows replacing the PR reference with a repository qualified reference, | 
|  | /// so that GitHub auto links to the correct repository instead of | 
|  | /// an irrelevant issue or PR on the SDK repository. | 
|  | final RegExp _mergeCommitPullRequestReference = RegExp(r'\(#?(\d+)\)$'); | 
|  |  | 
|  | /// A regex that matches any non-qualified issue or PR references | 
|  | /// within a commit message, such as `#123`. | 
|  | /// | 
|  | /// Allows replacing or wrapping the potential issue or PR references so that | 
|  | /// GitHub doesn't autolink to an irrelevant issue or PR on the SDK repository. | 
|  | final RegExp _issueHashReference = RegExp(r'\B#\d+'); | 
|  |  | 
|  | // By convention, pinned deps are deps with an eol comment. | 
|  | Set<String> calculatePinnedDeps() { | 
|  | final packageRevision = RegExp(r'"(\w+)_rev":'); | 
|  |  | 
|  | // "markdown_rev": "e3f4bd28c9...cfeccd83ee", # b/236358256 | 
|  | var depsFile = File('DEPS'); | 
|  | return depsFile | 
|  | .readAsLinesSync() | 
|  | .where((line) => packageRevision.hasMatch(line) && line.contains('", #')) | 
|  | .map((line) => packageRevision.firstMatch(line)!.group(1)!) | 
|  | .toSet(); | 
|  | } | 
|  |  | 
|  | class GitHelper { | 
|  | final String dir; | 
|  |  | 
|  | GitHelper(this.dir); | 
|  |  | 
|  | Future<String> fetch() { | 
|  | return exec(['git', 'fetch'], cwd: dir); | 
|  | } | 
|  |  | 
|  | Future<String> findLatestUnsyncedCommit() async { | 
|  | // git log ..origin/<default-branch> --format=%H -1 | 
|  |  | 
|  | var result = await exec( | 
|  | [ | 
|  | 'git', | 
|  | 'log', | 
|  | '..origin/$defaultBranchName', | 
|  | '--format=%H', | 
|  | '-1', | 
|  | ], | 
|  | cwd: dir, | 
|  | ); | 
|  | return result.trim(); | 
|  | } | 
|  |  | 
|  | Future<String> calculateUnsyncedCommits() async { | 
|  | // git log ..origin/<default-branch> --format="%h  %ad  %aN  %s" -1 | 
|  | var result = await exec( | 
|  | [ | 
|  | 'git', | 
|  | 'log', | 
|  | '..origin/$defaultBranchName', | 
|  | '--format=%h  %ad  %aN  %s', | 
|  | ], | 
|  | cwd: dir, | 
|  | ); | 
|  | return result.trim(); | 
|  | } | 
|  |  | 
|  | String get defaultBranchName { | 
|  | var branchNames = Directory(path.join(dir, '.git', 'refs', 'heads')) | 
|  | .listSync() | 
|  | .whereType<File>() | 
|  | .map((f) => path.basename(f.path)) | 
|  | .toSet(); | 
|  |  | 
|  | for (var name in ['main', 'master']) { | 
|  | if (branchNames.contains(name)) { | 
|  | return name; | 
|  | } | 
|  | } | 
|  |  | 
|  | return 'main'; | 
|  | } | 
|  | } | 
|  |  | 
|  | class GClientHelper { | 
|  | Future<List<PackageDependency>> getPackageDependencies() async { | 
|  | // gclient revinfo --output-json=<file> --ignore-dep-type=cipd | 
|  |  | 
|  | final tempDir = Directory.systemTemp.createTempSync(); | 
|  | final outFile = File(path.join(tempDir.path, 'deps.json')); | 
|  |  | 
|  | await exec([ | 
|  | 'gclient', | 
|  | 'revinfo', | 
|  | '--output-json=${outFile.path}', | 
|  | '--ignore-dep-type=cipd', | 
|  | ]); | 
|  | Map<String, dynamic> m = jsonDecode(outFile.readAsStringSync()); | 
|  | tempDir.deleteSync(recursive: true); | 
|  |  | 
|  | return m.entries.map((entry) { | 
|  | return PackageDependency( | 
|  | entry: entry.key, | 
|  | url: (entry.value as Map)['url'], | 
|  | rev: (entry.value as Map)['rev'], | 
|  | ); | 
|  | }).where((PackageDependency deps) { | 
|  | return deps.entry.startsWith('sdk/third_party/pkg/'); | 
|  | }).toList(); | 
|  | } | 
|  |  | 
|  | Future<String> getHash(PackageDependency dep) async { | 
|  | // DEPOT_TOOLS_UPDATE=0 gclient getdep --var=path_rev | 
|  | var depName = dep.name; | 
|  | var result = await exec( | 
|  | [ | 
|  | 'gclient', | 
|  | 'getdep', | 
|  | '--var=${depName}_rev', | 
|  | ], | 
|  | environment: { | 
|  | 'DEPOT_TOOLS_UPDATE': '0', | 
|  | }, | 
|  | ); | 
|  | return result.trim(); | 
|  | } | 
|  |  | 
|  | Future<String> setHash(PackageDependency dep, String hash) async { | 
|  | // gclient setdep --var=args_rev=9879dsf7g9d87d9f8g7 | 
|  | var depName = dep.name; | 
|  | return await exec( | 
|  | [ | 
|  | 'gclient', | 
|  | 'setdep', | 
|  | '--var=${depName}_rev=$hash', | 
|  | ], | 
|  | environment: { | 
|  | 'DEPOT_TOOLS_UPDATE': '0', | 
|  | }, | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | class PackageDependency { | 
|  | final String entry; | 
|  | final String url; | 
|  | final String? rev; | 
|  |  | 
|  | PackageDependency({ | 
|  | required this.entry, | 
|  | required this.url, | 
|  | required this.rev, | 
|  | }); | 
|  |  | 
|  | String get name => entry.substring(entry.lastIndexOf('/') + 1); | 
|  |  | 
|  | String get relativePath => entry.substring('sdk/'.length); | 
|  |  | 
|  | /// The identifier of the GitHub repository this dependency is from. | 
|  | /// | 
|  | /// For example: `dart-lang/test`. | 
|  | String get gitHubRepoIdentifier { | 
|  | var repo = url.substring(url.lastIndexOf('/') + 1); | 
|  | if (repo.endsWith('git')) { | 
|  | repo = repo.substring(0, repo.length - '.git'.length); | 
|  | } | 
|  |  | 
|  | final String org; | 
|  | if (url.contains('/external/')) { | 
|  | // https://dart.googlesource.com/external/github.com/google/webdriver.dart.git | 
|  | final parts = url.split('/'); | 
|  | org = parts[parts.length - 2]; | 
|  | } else { | 
|  | org = 'dart-lang'; | 
|  | } | 
|  |  | 
|  | return '$org/$repo'; | 
|  | } | 
|  |  | 
|  | /// The URL of the GitHub comparison view between [fromCommit] and [toCommit]. | 
|  | Uri gitHubDiffUrl(String fromCommit, String toCommit) { | 
|  | // https://github.com/dart-lang/<repo>/compare/<old>..<new> | 
|  | final from = fromCommit.substring(0, 7); | 
|  | final to = toCommit.substring(0, 7); | 
|  |  | 
|  | return Uri.https('github.com', '$gitHubRepoIdentifier/compare/$from..$to'); | 
|  | } | 
|  |  | 
|  | @override | 
|  | String toString() => '${rev?.substring(0, 8)} $relativePath'; | 
|  | } | 
|  |  | 
|  | Future<String> exec( | 
|  | List<String> cmd, { | 
|  | String? cwd, | 
|  | Map<String, String>? environment, | 
|  | }) async { | 
|  | var result = await Process.run( | 
|  | cmd.first, | 
|  | cmd.sublist(1), | 
|  | workingDirectory: cwd, | 
|  | environment: environment, | 
|  | ); | 
|  | if (result.exitCode != 0) { | 
|  | var cwdLocation = cwd == null ? '' : ' ($cwd)'; | 
|  | print('${cmd.join(' ')}$cwdLocation'); | 
|  |  | 
|  | if ((result.stdout as String).isNotEmpty) { | 
|  | stdout.write(result.stdout); | 
|  | } | 
|  | if ((result.stderr as String).isNotEmpty) { | 
|  | stderr.write(result.stderr); | 
|  | } | 
|  | exit(1); | 
|  | } | 
|  | return result.stdout; | 
|  | } |