blob: f17ea43fd0cc173208a74c8a1014425ce5dbf64b [file] [log] [blame]
// Copyright 2024 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"
"slices"
"strconv"
"strings"
"github.com/google/safehtml"
"github.com/google/safehtml/template"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/htmlutil"
"golang.org/x/oscar/internal/labels"
)
// labelsPage holds the fields needed to display the results
// of an issue categorization.
type labelsPage struct {
CommonPage
Params labelsParams // the raw parameters
Results []*labelsResult
Error error // if non-nil, the error to display instead of the result
}
type labelsResult struct {
*github.Issue // the issue we're reporting on
Problem string
Category labels.Category
Explanation string
BodyHTML safehtml.HTML
}
// labelsParams holds the raw inputs to the labels form.
type labelsParams struct {
Query string // the issue ID to lookup
}
func (g *Gaby) handleLabels(w http.ResponseWriter, r *http.Request) {
handlePage(w, g.populateLabelsPage(r), labelsPageTmpl)
}
var labelsPageTmpl = newTemplate(labelsPageTmplFile, template.FuncMap{})
// populateLabelsPage returns the contents of the labels page.
func (g *Gaby) populateLabelsPage(r *http.Request) *labelsPage {
pm := labelsParams{
Query: r.FormValue(paramQuery),
}
p := &labelsPage{
Params: pm,
}
p.setCommonPage()
if pm.Query == "" {
return p
}
var project string
if len(g.githubProjects) > 0 {
project = g.githubProjects[0] // default to first project
}
var issueMin, issueMax int64
smin, smax, ok := strings.Cut(pm.Query, ",")
if ok {
var err1, err2 error
issueMin, err1 = strconv.ParseInt(smin, 10, 64)
issueMax, err2 = strconv.ParseInt(smax, 10, 64)
if err := errors.Join(err1, err2); err != nil {
p.Error = err
return p
}
} else {
proj, issue, err := parseIssueNumber(pm.Query)
if err != nil {
p.Error = fmt.Errorf("invalid form value %q: %w", pm.Query, err)
return p
}
if proj != "" {
if !slices.Contains(g.githubProjects, proj) {
p.Error = fmt.Errorf("invalid form value (unrecognized project): %q", pm.Query)
return p
}
project = proj
}
issueMin = issue
issueMax = issue
}
// Find issues in database.
for i := range github.LookupIssues(g.db, project, issueMin, issueMax) {
lr := &labelsResult{Issue: i}
if i.PullRequest != nil {
lr.Problem = "skipping: pull request"
} else if isBot(i.User.Login) {
lr.Problem = "skipping: author is a bot"
} else {
cat, exp, err := labels.IssueCategory(r.Context(), g.db, g.llm, i)
if err != nil {
p.Error = err
return p
}
lr.Category = cat
lr.Explanation = exp
// Should probably use GH-flavored markdown here, but it's only for display.
lr.BodyHTML = htmlutil.MarkdownToSafeHTML(i.Body)
}
p.Results = append(p.Results, lr)
}
return p
}
func (p *labelsPage) setCommonPage() {
p.CommonPage = CommonPage{
ID: labelsID,
Description: "Categorize issues.",
Styles: []safeURL{labelsID.CSS()},
Form: Form{
Inputs: p.Params.inputs(),
SubmitText: "categorize",
},
}
}
func (pm *labelsParams) inputs() []FormInput {
return []FormInput{
{
Label: "issue",
Type: "int, int,int or string",
Description: "the issue(s) to check, as a number, two numbers, or URL (e.g. 1234, golang/go#1234, or https://github.com/golang/go/issues/1234)",
Name: safeQuery,
Required: true,
Typed: TextInput{
ID: safeQuery,
Value: pm.Query,
},
},
}
}
func isBot(author string) bool {
// TODO: generalize.
return author == "gopherbot" || author == "gabyhelp"
}