[infra] Add tools/bots/find_base_commit.dart

The find_base_commit program locates the newest commit with a full set of
test results. This is a good base for local testing when comparing with the
results from the official bots, as all the needed results are there.

Change-Id: Ie06020ca98317b82f46589ad6dede37361acf744
Reviewed-on: https://dart-review.googlesource.com/c/81040
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/tools/bots/find_base_commit.dart b/tools/bots/find_base_commit.dart
new file mode 100755
index 0000000..2df7e35
--- /dev/null
+++ b/tools/bots/find_base_commit.dart
@@ -0,0 +1,116 @@
+#!/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 bots.
+
+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 = new ArgParser();
+  parser.addMultiOption("bot",
+      abbr: "b",
+      help: "Select the bots matching the glob pattern [option is repeatable]",
+      splitCommas: false);
+  parser.addOption("branch",
+      abbr: "B",
+      help: "Select the bots building this branch",
+      defaultsTo: "master");
+  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["help"]) {
+    print("""
+Usage: find_base_commit.dart [OPTION]...
+Find the newest commit that has a full set of results on the bots.
+
+The options are as follows:
+
+${parser.usage}""");
+    return;
+  }
+
+  int count = int.parse(options["count"]);
+  final globs = new List<Glob>.from(
+      options["bot"].map((String pattern) => new Glob(pattern)));
+
+  // Download the most recent builds from buildbucket.
+  int maxBuilds = 1000;
+  final url = Uri.parse(
+      "https://cr-buildbucket.appspot.com/_ah/api/buildbucket/v1/search"
+      "?bucket=luci.dart.ci.sandbox"
+      "&max_builds=$maxBuilds"
+      "&status=COMPLETED"
+      "&fields=builds(url%2Cparameters_json)");
+  final client = new HttpClient();
+  final request = await client.getUrl(url).timeout(const Duration(seconds: 30));
+  final response = await request.close().timeout(const Duration(seconds: 30));
+  final Map<String, dynamic> object = await response
+      .transform(new Utf8Decoder())
+      .transform(new JsonDecoder())
+      .first
+      .timeout(const Duration(seconds: 30));
+  client.close();
+
+  // 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 bots
+  // 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 botsForCommits map will
+  // then sorted with the newest commit first.
+  final builds = object["builds"];
+  final botsForCommits = <String, Set<String>>{};
+  for (final build in builds) {
+    final parameters = jsonDecode(build["parameters_json"]);
+    final bot = parameters["builder_name"];
+    if (bot.endsWith("-dev") || bot.endsWith("-stable")) {
+      // Ignore the -dev and -stable builders. The -try builders aren't in the
+      // bucket we're reading.
+      continue;
+    }
+    if (globs.isNotEmpty && !globs.any((glob) => glob.matches(bot))) {
+      // Filter way bots we're not interested in.
+      continue;
+    }
+    final properties = parameters["properties"];
+    final branch = properties["branch"];
+    if (branch != null && branch != "refs/heads/${options['branch']}") {
+      // Ignore bots that are building the wrong branch.
+      continue;
+    }
+    final commit = properties["revision"];
+    if (commit == null) {
+      // Ignore bots that aren't commit based, e.g. fuzz-linux.
+      continue;
+    }
+    final botsForCommit =
+        botsForCommits.putIfAbsent(commit, () => new Set<String>());
+    botsForCommit.add(bot);
+  }
+
+  if (botsForCommits.isEmpty) {
+    print("Failed to locate any commits having run on the bots");
+    exitCode = 1;
+    return;
+  }
+
+  int maxBots = 0;
+  for (final commit in botsForCommits.keys) {
+    maxBots = max(maxBots, botsForCommits[commit].length);
+  }
+
+  // List commits run on the most bots.
+  for (final commit in botsForCommits.keys
+      .where((commit) => botsForCommits[commit].length == maxBots)
+      .take(count)) {
+    print(commit);
+  }
+}