Eric Boren | d75fdc6 | 2018-01-11 13:45:17 -0500 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | package main |
| 6 | |
| 7 | /* |
| 8 | Tool for bisecting failed rolls. |
| 9 | */ |
| 10 | |
| 11 | import ( |
| 12 | "bufio" |
| 13 | "context" |
| 14 | "flag" |
| 15 | "fmt" |
| 16 | "os" |
| 17 | "os/exec" |
| 18 | "os/user" |
| 19 | "path" |
| 20 | "strings" |
| 21 | "time" |
| 22 | |
| 23 | "go.skia.org/infra/autoroll/go/repo_manager" |
| 24 | "go.skia.org/infra/go/autoroll" |
| 25 | "go.skia.org/infra/go/common" |
| 26 | "go.skia.org/infra/go/gerrit" |
| 27 | "go.skia.org/infra/go/util" |
| 28 | ) |
| 29 | |
| 30 | var ( |
| 31 | // Flags. |
| 32 | autoRollerAccount = flag.String("autoroller_account", "skia-deps-roller@chromium.org", "Email address of the autoroller.") |
| 33 | childPath = flag.String("childPath", "src/third_party/skia", "Path within parent repo of the project to roll.") |
| 34 | gerritUrl = flag.String("gerrit", "https://chromium-review.googlesource.com", "URL of the Gerrit server.") |
| 35 | parentRepoUrl = flag.String("parent_repo_url", common.REPO_CHROMIUM, "URL of the parent repo (the child repo rolls into this repo).") |
| 36 | workdir = flag.String("workdir", path.Join(os.TempDir(), "autoroll_bisect"), "Working directory.") |
| 37 | ) |
| 38 | |
| 39 | func log(tmpl string, a ...interface{}) { |
| 40 | fmt.Println(fmt.Sprintf(tmpl, a...)) |
| 41 | } |
| 42 | |
| 43 | func bail(a ...interface{}) { |
| 44 | fmt.Fprintln(os.Stderr, a...) |
| 45 | os.Exit(1) |
| 46 | } |
| 47 | |
| 48 | func main() { |
| 49 | // Setup. |
| 50 | common.Init() |
| 51 | ctx := context.Background() |
| 52 | |
| 53 | log("Updating repos and finding roll attempts; this can take a few minutes...") |
| 54 | |
| 55 | // Create the working directory if necessary. |
| 56 | if err := os.MkdirAll(*workdir, os.ModePerm); err != nil { |
| 57 | bail(err) |
| 58 | } |
| 59 | |
| 60 | // Create the RepoManager. |
| 61 | gclient, err := exec.LookPath("gclient") |
| 62 | if err != nil { |
| 63 | bail(err) |
| 64 | } |
| 65 | depotTools := path.Dir(gclient) |
| 66 | user, err := user.Current() |
| 67 | if err != nil { |
| 68 | bail(err) |
| 69 | } |
| 70 | gitcookiesPath := path.Join(user.HomeDir, ".gitcookies") |
| 71 | g, err := gerrit.NewGerrit(*gerritUrl, gitcookiesPath, nil) |
| 72 | if err != nil { |
| 73 | bail("Failed to create Gerrit client:", err) |
| 74 | } |
| 75 | g.TurnOnAuthenticatedGets() |
| 76 | childBranch := "master" |
| 77 | strat, err := repo_manager.GetNextRollStrategy(repo_manager.ROLL_STRATEGY_BATCH, childBranch, "") |
| 78 | if err != nil { |
| 79 | bail(err) |
| 80 | } |
| 81 | rm, err := repo_manager.NewDEPSRepoManager(ctx, *workdir, *parentRepoUrl, "master", *childPath, childBranch, depotTools, g, strat, nil, true, nil, "(local run)") |
| 82 | if err != nil { |
| 83 | bail(err) |
| 84 | } |
| 85 | |
| 86 | // Determine the set of not-yet-rolled commits. |
| 87 | lastRoll := rm.LastRollRev() |
| 88 | nextRoll := rm.NextRollRev() |
| 89 | commits, err := rm.ChildRevList(ctx, fmt.Sprintf("%s..%s", lastRoll, nextRoll)) |
| 90 | if err != nil { |
| 91 | bail(err) |
| 92 | } |
| 93 | if len(commits) == 0 { |
| 94 | log("Repo is up-to-date.") |
| 95 | os.Exit(0) |
| 96 | } else if len(commits) == 1 { |
| 97 | log("Recommend reverting commit %s", commits[0]) |
| 98 | os.Exit(0) |
| 99 | } |
| 100 | |
| 101 | // Next, find any failed roll CLs. |
| 102 | // TODO(borenet): Use the timestamp of the last-rolled commit. |
| 103 | lastRollTime := time.Now().Add(-24 * time.Hour) |
| 104 | modAfter := gerrit.SearchModifiedAfter(lastRollTime) |
| 105 | cls, err := g.Search(500, modAfter, gerrit.SearchOwner(*autoRollerAccount)) |
| 106 | if err != nil { |
| 107 | bail(err) |
| 108 | } |
| 109 | cls2, err := g.Search(500, modAfter, gerrit.SearchOwner("self")) |
| 110 | if err != nil { |
| 111 | bail(err) |
| 112 | } |
| 113 | cls = append(cls, cls2...) |
| 114 | |
| 115 | // Filter out CLs which don't look like rolls, de-duplicate CLs which |
| 116 | // roll to the same commit, taking the most recent. |
| 117 | rollCls := make(map[string]*autoroll.AutoRollIssue, len(cls)) |
| 118 | fullHashFn := func(hash string) (string, error) { |
| 119 | return rm.FullChildHash(ctx, hash) |
| 120 | } |
| 121 | for _, cl := range cls { |
| 122 | issue, err := autoroll.FromGerritChangeInfo(cl, fullHashFn, false) |
| 123 | if err == nil { |
| 124 | if old, ok := rollCls[issue.RollingTo]; !ok || ok && issue.Modified.After(old.Modified) { |
| 125 | rollCls[issue.RollingTo] = issue |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // Report the summary of the not-rolled commits and their associated |
| 131 | // roll results to the user. |
| 132 | log("%d commits have not yet rolled:", len(commits)) |
| 133 | earliestFail := -1 |
| 134 | latestFail := -1 |
| 135 | latestSuccess := -1 // eg. dry runs. |
| 136 | for idx, commit := range commits { |
| 137 | if cl, ok := rollCls[commit]; ok { |
| 138 | log("%s roll %s", commit[:12], cl.Result) |
| 139 | if util.In(cl.Result, autoroll.FAILURE_RESULTS) { |
| 140 | earliestFail = idx |
| 141 | if latestFail == -1 { |
| 142 | latestFail = idx |
| 143 | } |
| 144 | } else if util.In(cl.Result, autoroll.SUCCESS_RESULTS) && latestSuccess == -1 { |
| 145 | latestSuccess = idx |
| 146 | } |
| 147 | } else { |
| 148 | log(commit[:12]) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // Suggest a commit to try rolling. The user may choose a different one. |
| 153 | suggestedCommit := "" |
| 154 | if latestSuccess != -1 { |
| 155 | suggestedCommit = commits[latestSuccess] |
| 156 | log("Recommend landing successful roll %s/%d", *gerritUrl, rollCls[suggestedCommit].Issue) |
| 157 | } else if latestFail != 0 { |
| 158 | suggestedCommit = commits[0] |
| 159 | if issue, ok := rollCls[suggestedCommit]; ok && issue.Result == autoroll.ROLL_RESULT_IN_PROGRESS { |
| 160 | log("Recommend waiting for the current in-progress roll to finish: %s/%d", *gerritUrl, issue.Issue) |
| 161 | suggestedCommit = "" |
| 162 | } else { |
| 163 | log("Recommend trying a roll at %s which has not yet been tried.", suggestedCommit) |
| 164 | } |
| 165 | } else if earliestFail == 0 { |
| 166 | log("Recommend reverting commit %s", commits[earliestFail]) |
| 167 | } else { |
| 168 | // Bisect the commits which have not yet failed. |
| 169 | remaining := commits[earliestFail+1:] |
| 170 | idx := len(remaining) / 2 |
| 171 | suggestedCommit = remaining[idx] |
| 172 | log("Recommend trying a roll at %s", suggestedCommit) |
| 173 | } |
| 174 | |
| 175 | // Ask the user what commit to roll. |
| 176 | msg := "Type a commit hash to roll" |
| 177 | if suggestedCommit != "" { |
| 178 | msg += fmt.Sprintf(" (press enter to roll at suggested commit %s)", suggestedCommit[:12]) |
| 179 | } |
| 180 | log("%s:", msg) |
| 181 | reader := bufio.NewReader(os.Stdin) |
| 182 | text, err := reader.ReadString('\n') |
| 183 | if err != nil { |
| 184 | bail(err) |
| 185 | } |
| 186 | text = strings.TrimSpace(text) |
| 187 | if text == "" && suggestedCommit != "" { |
| 188 | text = suggestedCommit |
| 189 | } |
| 190 | if text == "" { |
| 191 | bail("You must enter a commit hash.") |
| 192 | } |
| 193 | log("Attempting a roll at %q", text) |
| 194 | rollTo, err := rm.FullChildHash(ctx, text) |
| 195 | if err != nil { |
| 196 | bail(text, "is not a valid commit hash:", text, err) |
| 197 | } |
| 198 | |
| 199 | // Upload a roll. |
| 200 | email, err := g.GetUserEmail() |
| 201 | if err != nil { |
| 202 | bail(err) |
| 203 | } |
| 204 | issue, err := rm.CreateNewRoll(ctx, lastRoll, rollTo, []string{email}, "", false) |
| 205 | if err != nil { |
| 206 | bail(err) |
| 207 | } |
| 208 | log("Uploaded %s/%d", *gerritUrl, issue) |
| 209 | } |