| // 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; |
| } |