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