Home | History | Annotate | Download | only in tools
      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 (a] 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 }
    210