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
5package main
6
7/*
8   Tool for bisecting failed rolls.
9*/
10
11import (
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
30var (
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
39func log(tmpl string, a ...interface{}) {
40	fmt.Println(fmt.Sprintf(tmpl, a...))
41}
42
43func bail(a ...interface{}) {
44	fmt.Fprintln(os.Stderr, a...)
45	os.Exit(1)
46}
47
48func 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}
210