| // Copyright 2025 The Go 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 |
| |
| import ( |
| "errors" |
| "fmt" |
| "net/http" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "golang.org/x/oscar/internal/bisect" |
| "golang.org/x/oscar/internal/github" |
| ) |
| |
| // bisectLogPage is the data for the bisect log HTML template. |
| type bisectLogPage struct { |
| CommonPage |
| |
| Tasks []*bisect.Task // all bisection tasks |
| } |
| |
| var bisectLogPageTmpl = newTemplate(bisectLogTmplFile, nil) |
| |
| func (g *Gaby) handleBisectLog(w http.ResponseWriter, r *http.Request) { |
| var p bisectLogPage |
| for _, t := range g.bisect.BisectionTasks() { |
| p.Tasks = append(p.Tasks, t) |
| } |
| // Sort the tasks by creation time, from newest to oldest. |
| sort.SliceStable(p.Tasks, func(i, j int) bool { |
| return p.Tasks[i].Created.After(p.Tasks[j].Created) |
| }) |
| p.setCommonPage() |
| |
| b, err := Exec(bisectLogPageTmpl, &p) |
| if err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| _, _ = w.Write(b) |
| } |
| |
| func (p *bisectLogPage) setCommonPage() { |
| p.CommonPage = CommonPage{ |
| ID: bisectlogID, |
| Description: "Browse bisection tasks performed by Oscar.", |
| Form: Form{ |
| Inputs: nil, |
| SubmitText: "void", |
| }, |
| } |
| } |
| |
| // parseBisectTrigger extracts a bisection Request, if any, |
| // from a GitHub issue comment trigger. The expected request |
| // format for the comment body is as follows: |
| // |
| // - there can be empty lines |
| // - the first non-empty line is "@gabyhelp bisect [bad] [good]". |
| // Both bad and good are optional, but if one is present so must |
| // be the other. |
| // - the regression body comes after and it is enclosed with |
| // triple backticks (```). |
| // |
| // An error is returned if the trigger contains a bisect directive |
| // but the rest of the comment is not properly formatted. |
| func parseBisectTrigger(trigger *github.WebhookIssueCommentEvent) (*bisect.Request, error) { |
| body := trigger.Comment.Body |
| |
| // Find the bisection directive line, if any. |
| lines := strings.Split(body, "\n") |
| i, bad, good, err := bisectDirectiveLine(lines) |
| if err != nil { |
| return nil, err |
| } |
| if i == -1 { |
| return nil, nil |
| } |
| // If bad and good commits are not provided, |
| // set them to default values. |
| if bad == "" { |
| bad = "master" |
| } |
| if good == "" { |
| good = "go1.22.0" |
| } |
| |
| // Extract the regression code from the rest |
| // of the body. |
| rest := strings.Join(lines[i+1:], "\n") |
| regression := strings.TrimSpace(bisectRegression(rest)) |
| if regression == "" { |
| return nil, errors.New("missing bisect regression") |
| } |
| return &bisect.Request{ |
| Trigger: trigger.Comment.URL, |
| Issue: trigger.Issue.URL, |
| Repo: "https://go.googlesource.com/go", |
| Fail: bad, |
| Pass: good, |
| Body: regression, |
| }, nil |
| } |
| |
| // bisectDirectiveLine checks if there is a line |
| // encoding a bisection directive. If so, it |
| // returns the position of the line and it extracts |
| // the bisect commits from the line. If the line |
| // is not properly formatted, returns an error. |
| func bisectDirectiveLine(lines []string) (int, string, string, error) { |
| for i, l := range lines { |
| l = strings.TrimSpace(l) |
| fs := strings.Fields(l) |
| if len(fs) < 2 { |
| continue |
| } |
| if fs[0] != "@gabyhelp" || fs[1] != "bisect" { |
| continue |
| } |
| // Bisect directive identified, check for |
| // commits and errors. |
| if len(fs) == 2 { |
| // Only bisect directive is present. |
| return i, "", "", nil |
| } |
| if len(fs) == 4 { |
| // Both the bisect directive and |
| // two commits are present. |
| return i, fs[2], fs[3], nil |
| } |
| return i, "", "", fmt.Errorf("bisect directive not properly formatted: %s", l) |
| |
| } |
| return -1, "", "", nil |
| } |
| |
| // regressionRegexp matches any text, including |
| // newlines, that is surrounded by triple backticks. |
| var regressionRegexp = regexp.MustCompile("(?s)```(.+)```") |
| |
| // bisectRegression extracts a regression test |
| // case from body. |
| func bisectRegression(body string) string { |
| matches := regressionRegexp.FindAllStringSubmatch(body, -1) |
| if len(matches) != 1 { |
| return "" |
| } |
| rmatches := matches[0] |
| if len(rmatches) != 2 { |
| return "" |
| } |
| return rmatches[1] |
| } |