Add bisect_roll tool
1. Loads the list of commits which have not yet rolled.
2. Loads the recent roll attempts.
3. Suggests a commit to try.
4. Uploads a roll CL at the commit specified by the user.
Bug: skia:
Change-Id: I68a7f6dbbca638cc9f17bad4eed71e889fe43b6e
Reviewed-on: https://skia-review.googlesource.com/93580
Commit-Queue: Eric Boren <borenet@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/tools/bisect_roll b/tools/bisect_roll
new file mode 100755
index 0000000..4c3a620
--- /dev/null
+++ b/tools/bisect_roll
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# Copyright 2018 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+base_dir=$(dirname "$0")
+go run "$base_dir/bisect_roll.go" "$@"
diff --git a/tools/bisect_roll.bat b/tools/bisect_roll.bat
new file mode 100644
index 0000000..a7ad00a
--- /dev/null
+++ b/tools/bisect_roll.bat
@@ -0,0 +1,6 @@
+@echo off
+:: Copyright 2018 Google Inc.
+::
+:: Use of this source code is governed by a BSD-style license that can be
+:: found in the LICENSE file.
+go run "%~dp0\bisect_roll.go" %*
diff --git a/tools/bisect_roll.go b/tools/bisect_roll.go
new file mode 100644
index 0000000..3bc4262
--- /dev/null
+++ b/tools/bisect_roll.go
@@ -0,0 +1,209 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+/*
+ Tool for bisecting failed rolls.
+*/
+
+import (
+ "bufio"
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "os/user"
+ "path"
+ "strings"
+ "time"
+
+ "go.skia.org/infra/autoroll/go/repo_manager"
+ "go.skia.org/infra/go/autoroll"
+ "go.skia.org/infra/go/common"
+ "go.skia.org/infra/go/gerrit"
+ "go.skia.org/infra/go/util"
+)
+
+var (
+ // Flags.
+ autoRollerAccount = flag.String("autoroller_account", "skia-deps-roller@chromium.org", "Email address of the autoroller.")
+ childPath = flag.String("childPath", "src/third_party/skia", "Path within parent repo of the project to roll.")
+ gerritUrl = flag.String("gerrit", "https://chromium-review.googlesource.com", "URL of the Gerrit server.")
+ parentRepoUrl = flag.String("parent_repo_url", common.REPO_CHROMIUM, "URL of the parent repo (the child repo rolls into this repo).")
+ workdir = flag.String("workdir", path.Join(os.TempDir(), "autoroll_bisect"), "Working directory.")
+)
+
+func log(tmpl string, a ...interface{}) {
+ fmt.Println(fmt.Sprintf(tmpl, a...))
+}
+
+func bail(a ...interface{}) {
+ fmt.Fprintln(os.Stderr, a...)
+ os.Exit(1)
+}
+
+func main() {
+ // Setup.
+ common.Init()
+ ctx := context.Background()
+
+ log("Updating repos and finding roll attempts; this can take a few minutes...")
+
+ // Create the working directory if necessary.
+ if err := os.MkdirAll(*workdir, os.ModePerm); err != nil {
+ bail(err)
+ }
+
+ // Create the RepoManager.
+ gclient, err := exec.LookPath("gclient")
+ if err != nil {
+ bail(err)
+ }
+ depotTools := path.Dir(gclient)
+ user, err := user.Current()
+ if err != nil {
+ bail(err)
+ }
+ gitcookiesPath := path.Join(user.HomeDir, ".gitcookies")
+ g, err := gerrit.NewGerrit(*gerritUrl, gitcookiesPath, nil)
+ if err != nil {
+ bail("Failed to create Gerrit client:", err)
+ }
+ g.TurnOnAuthenticatedGets()
+ childBranch := "master"
+ strat, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, childBranch, "")
+ if err != nil {
+ bail(err)
+ }
+ rm, err := repo_manager.NewDEPSRepoManager(ctx, *workdir, *parentRepoUrl, "master", *childPath, childBranch, depotTools, g, strat, nil, true, nil, "(local run)")
+ if err != nil {
+ bail(err)
+ }
+
+ // Determine the set of not-yet-rolled commits.
+ lastRoll := rm.LastRollRev()
+ nextRoll := rm.NextRollRev()
+ commits, err := rm.ChildRevList(ctx, fmt.Sprintf("%s..%s", lastRoll, nextRoll))
+ if err != nil {
+ bail(err)
+ }
+ if len(commits) == 0 {
+ log("Repo is up-to-date.")
+ os.Exit(0)
+ } else if len(commits) == 1 {
+ log("Recommend reverting commit %s", commits[0])
+ os.Exit(0)
+ }
+
+ // Next, find any failed roll CLs.
+ // TODO(borenet): Use the timestamp of the last-rolled commit.
+ lastRollTime := time.Now().Add(-24 * time.Hour)
+ modAfter := gerrit.SearchModifiedAfter(lastRollTime)
+ cls, err := g.Search(500, modAfter, gerrit.SearchOwner(*autoRollerAccount))
+ if err != nil {
+ bail(err)
+ }
+ cls2, err := g.Search(500, modAfter, gerrit.SearchOwner("self"))
+ if err != nil {
+ bail(err)
+ }
+ cls = append(cls, cls2...)
+
+ // Filter out CLs which don't look like rolls, de-duplicate CLs which
+ // roll to the same commit, taking the most recent.
+ rollCls := make(map[string]*autoroll.AutoRollIssue, len(cls))
+ fullHashFn := func(hash string) (string, error) {
+ return rm.FullChildHash(ctx, hash)
+ }
+ for _, cl := range cls {
+ issue, err := autoroll.FromGerritChangeInfo(cl, fullHashFn, false)
+ if err == nil {
+ if old, ok := rollCls[issue.RollingTo]; !ok || ok && issue.Modified.After(old.Modified) {
+ rollCls[issue.RollingTo] = issue
+ }
+ }
+ }
+
+ // Report the summary of the not-rolled commits and their associated
+ // roll results to the user.
+ log("%d commits have not yet rolled:", len(commits))
+ earliestFail := -1
+ latestFail := -1
+ latestSuccess := -1 // eg. dry runs.
+ for idx, commit := range commits {
+ if cl, ok := rollCls[commit]; ok {
+ log("%s roll %s", commit[:12], cl.Result)
+ if util.In(cl.Result, autoroll.FAILURE_RESULTS) {
+ earliestFail = idx
+ if latestFail == -1 {
+ latestFail = idx
+ }
+ } else if util.In(cl.Result, autoroll.SUCCESS_RESULTS) && latestSuccess == -1 {
+ latestSuccess = idx
+ }
+ } else {
+ log(commit[:12])
+ }
+ }
+
+ // Suggest a commit to try rolling. The user may choose a different one.
+ suggestedCommit := ""
+ if latestSuccess != -1 {
+ suggestedCommit = commits[latestSuccess]
+ log("Recommend landing successful roll %s/%d", *gerritUrl, rollCls[suggestedCommit].Issue)
+ } else if latestFail != 0 {
+ suggestedCommit = commits[0]
+ if issue, ok := rollCls[suggestedCommit]; ok && issue.Result == autoroll.ROLL_RESULT_IN_PROGRESS {
+ log("Recommend waiting for the current in-progress roll to finish: %s/%d", *gerritUrl, issue.Issue)
+ suggestedCommit = ""
+ } else {
+ log("Recommend trying a roll at %s which has not yet been tried.", suggestedCommit)
+ }
+ } else if earliestFail == 0 {
+ log("Recommend reverting commit %s", commits[earliestFail])
+ } else {
+ // Bisect the commits which have not yet failed.
+ remaining := commits[earliestFail+1:]
+ idx := len(remaining) / 2
+ suggestedCommit = remaining[idx]
+ log("Recommend trying a roll at %s", suggestedCommit)
+ }
+
+ // Ask the user what commit to roll.
+ msg := "Type a commit hash to roll"
+ if suggestedCommit != "" {
+ msg += fmt.Sprintf(" (press enter to roll at suggested commit %s)", suggestedCommit[:12])
+ }
+ log("%s:", msg)
+ reader := bufio.NewReader(os.Stdin)
+ text, err := reader.ReadString('\n')
+ if err != nil {
+ bail(err)
+ }
+ text = strings.TrimSpace(text)
+ if text == "" && suggestedCommit != "" {
+ text = suggestedCommit
+ }
+ if text == "" {
+ bail("You must enter a commit hash.")
+ }
+ log("Attempting a roll at %q", text)
+ rollTo, err := rm.FullChildHash(ctx, text)
+ if err != nil {
+ bail(text, "is not a valid commit hash:", text, err)
+ }
+
+ // Upload a roll.
+ email, err := g.GetUserEmail()
+ if err != nil {
+ bail(err)
+ }
+ issue, err := rm.CreateNewRoll(ctx, lastRoll, rollTo, []string{email}, "", false)
+ if err != nil {
+ bail(err)
+ }
+ log("Uploaded %s/%d", *gerritUrl, issue)
+}