| #!/usr/bin/env dart | 
 | // Copyright (c) 2018, 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. | 
 |  | 
 | // Find the newest commit that has a full set of results on the builders. | 
 |  | 
 | import 'dart:async'; | 
 | import 'dart:convert'; | 
 | import 'dart:io'; | 
 | import 'dart:math'; | 
 |  | 
 | import 'package:args/args.dart'; | 
 | import 'package:glob/glob.dart'; | 
 |  | 
 | void main(List<String> args) async { | 
 |   final parser = ArgParser(); | 
 |   parser.addMultiOption("builder", | 
 |       abbr: "b", | 
 |       help: "Select the builders matching the glob [option is repeatable]", | 
 |       splitCommas: false); | 
 |   parser.addOption("branch", | 
 |       abbr: "B", | 
 |       help: "Select the builders building this branch", | 
 |       defaultsTo: "main"); | 
 |   parser.addOption("count", | 
 |       abbr: "c", help: "List this many commits", defaultsTo: "1"); | 
 |   parser.addFlag("help", help: "Show the program usage.", negatable: false); | 
 |  | 
 |   final options = parser.parse(args); | 
 |   if (options.flag("help")) { | 
 |     print(""" | 
 | Usage: find_base_commit.dart [OPTION]... | 
 | Find the newest commit that has a full set of results on the builders. | 
 |  | 
 | The options are as follows: | 
 |  | 
 | ${parser.usage}"""); | 
 |     return; | 
 |   } | 
 |  | 
 |   int count = int.parse(options.option("count")!); | 
 |   final globs = List<Glob>.from( | 
 |       options.multiOption("builder").map((String pattern) => Glob(pattern))); | 
 |  | 
 |   // Download the most recent builds from buildbucket. | 
 |   const maxBuilds = 1000; | 
 |   final url = Uri.parse("https://cr-buildbucket.appspot.com" | 
 |       "/prpc/buildbucket.v2.Builds/SearchBuilds"); | 
 |   const maxRetries = 3; | 
 |   const timeout = Duration(seconds: 30); | 
 |   final query = jsonEncode({ | 
 |     "predicate": { | 
 |       "builder": {"project": "dart", "bucket": "ci.sandbox"}, | 
 |       "status": "ENDED_MASK" | 
 |     }, | 
 |     "pageSize": maxBuilds, | 
 |     "fields": "builds.*.builder.builder,builds.*.input" | 
 |   }); | 
 |   late Map<String, dynamic> searchResult; | 
 |   for (int i = 1; i <= maxRetries; i++) { | 
 |     try { | 
 |       final client = HttpClient(); | 
 |       final request = await client.postUrl(url).timeout(timeout) | 
 |         ..headers.contentType = ContentType.json | 
 |         ..headers.add(HttpHeaders.acceptHeader, ContentType.json) | 
 |         ..write(query); | 
 |       final response = await request.close().timeout(timeout); | 
 |       if (response.statusCode != 200) { | 
 |         print("Failed to search for builds: " | 
 |             "${response.statusCode}:${response.reasonPhrase}"); | 
 |         exit(1); | 
 |       } | 
 |       const prefix = ")]}'"; | 
 |       searchResult = await (response | 
 |           .cast<List<int>>() | 
 |           .transform(Utf8Decoder()) | 
 |           .map((event) => | 
 |               event.startsWith(prefix) ? event.substring(prefix.length) : event) | 
 |           .transform(JsonDecoder()) | 
 |           .cast<Map<String, dynamic>>() | 
 |           .first | 
 |           .timeout(timeout)); | 
 |       client.close(); | 
 |       break; | 
 |     } on TimeoutException catch (e) { | 
 |       final inSeconds = e.duration?.inSeconds; | 
 |       stderr.writeln( | 
 |           "Attempt $i of $maxRetries timed out after $inSeconds seconds"); | 
 |       if (i == maxRetries) { | 
 |         stderr.writeln("error: Failed to download $url"); | 
 |         exit(1); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   // Locate the builds we're interested in and map them to each commit. The | 
 |   // builds returned by the API are sorted with the newest first. Since builders | 
 |   // don't build back in time and always build the latest commit whenever they | 
 |   // can, the first time we see a commit, we know it's newer than all commits | 
 |   // we haven't seen yet. The insertion order into the buildersForCommits map | 
 |   // will then sorted with the newest commit first. | 
 |   final builds = searchResult["builds"] as List?; | 
 |   if (builds == null) { | 
 |     print("No builds found"); | 
 |     exit(1); | 
 |   } | 
 |   final buildersForCommits = <String, Set<String>>{}; | 
 |   for (final build in builds) { | 
 |     final builder = (build["builder"] as Map?)?["builder"]; | 
 |     if (builder is! String || | 
 |         builder.endsWith("-beta") || | 
 |         builder.endsWith("-dev") || | 
 |         builder.endsWith("-stable")) { | 
 |       // Ignore the release builders. The -try builders aren't in the | 
 |       // bucket we're reading. | 
 |       continue; | 
 |     } | 
 |     if (globs.isNotEmpty && !globs.any((glob) => glob.matches(builder))) { | 
 |       // Filter way builders we're not interested in. | 
 |       continue; | 
 |     } | 
 |     final input = (build["input"] as Map?)?["gitilesCommit"] as Map?; | 
 |     if (input == null) { | 
 |       // Ignore builds not triggered by a commit, e.g. fuzz-linux. | 
 |       continue; | 
 |     } | 
 |     final ref = input["ref"]; | 
 |     if (ref != "refs/heads/${options.option('branch')}") { | 
 |       // Ignore builds on the wrong branch. | 
 |       continue; | 
 |     } | 
 |     final commit = input["id"] as String; | 
 |     final buildersForCommit = | 
 |         buildersForCommits.putIfAbsent(commit, () => <String>{}); | 
 |     buildersForCommit.add(builder); | 
 |   } | 
 |  | 
 |   if (buildersForCommits.isEmpty) { | 
 |     print("Failed to locate any commits having run on the builders"); | 
 |     exitCode = 1; | 
 |     return; | 
 |   } | 
 |  | 
 |   int maxBots = 0; | 
 |   for (final builders in buildersForCommits.values) { | 
 |     maxBots = max(maxBots, builders.length); | 
 |   } | 
 |  | 
 |   // List commits run on the most builders. | 
 |   for (final commit in buildersForCommits.keys | 
 |       .where((commit) => buildersForCommits[commit]!.length == maxBots) | 
 |       .take(count)) { | 
 |     print(commit); | 
 |   } | 
 | } |