blob: 6584dca85708cd9f25373517728141b006739472 [file] [log] [blame]
// 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;
}