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