blob: d5b0cb11883c5033f6a1650c25d69f3dd478a951 [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 (
"bufio"
"context"
"fmt"
"io"
"io/fs"
"strings"
"time"
"golang.org/x/build/gerrit"
"golang.org/x/build/maintner"
"golang.org/x/build/maintner/godata"
)
type ToDo struct {
message string // what is to be done
provenance string // where the TODO came from
}
// todo prints a report to w on which release notes need to be written.
// It takes the doc/next directory of the repo and the date of the last release.
func todo(w io.Writer, fsys fs.FS, prevRelDate time.Time) error {
var todos []ToDo
add := func(td ToDo) { todos = append(todos, td) }
if err := todosFromDocFiles(fsys, add); err != nil {
return err
}
if !prevRelDate.IsZero() {
if err := todosFromRelnoteCLs(prevRelDate, add); err != nil {
return err
}
}
return writeToDos(w, todos)
}
// Collect TODOs from the markdown files in the main repo.
func todosFromDocFiles(fsys fs.FS, add func(ToDo)) error {
// This is essentially a grep.
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(path, ".md") {
if err := todosFromFile(fsys, path, add); err != nil {
return err
}
}
return nil
})
}
func todosFromFile(dir fs.FS, filename string, add func(ToDo)) error {
f, err := dir.Open(filename)
if err != nil {
return err
}
defer f.Close()
scan := bufio.NewScanner(f)
ln := 0
for scan.Scan() {
ln++
if line := scan.Text(); strings.Contains(line, "TODO") {
add(ToDo{
message: line,
provenance: fmt.Sprintf("%s:%d", filename, ln),
})
}
}
return scan.Err()
}
func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error {
ctx := context.Background()
// The maintner corpus doesn't track inline comments. See go.dev/issue/24863.
// So we need to use a Gerrit API client to fetch them instead. If maintner starts
// tracking inline comments in the future, this extra complexity can be dropped.
gerritClient := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
matchedCLs, err := findCLsWithRelNote(gerritClient, cutoff)
if err != nil {
return err
}
corpus, err := godata.Get(ctx)
if err != nil {
return err
}
return corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
if gp.Server() != "go.googlesource.com" {
return nil
}
return gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
if cl.Status != "merged" {
return nil
}
if cl.Branch() != "master" {
// Ignore CLs sent to development or release branches.
return nil
}
if cl.Commit.CommitTime.Before(cutoff) {
// Was in a previous release; not for this one.
return nil
}
// TODO(jba): look for accepted proposals that don't have release notes.
if _, ok := matchedCLs[int(cl.Number)]; ok {
comments, err := gerritClient.ListChangeComments(context.Background(), fmt.Sprint(cl.Number))
if err != nil {
return err
}
if rn := clRelNote(cl, comments); rn != "" {
if rn == "yes" || rn == "y" {
rn = "UNKNOWN"
}
add(ToDo{
message: "TODO:" + rn,
provenance: fmt.Sprintf("RELNOTE comment in https://go.dev/cl/%d", cl.Number),
})
}
}
return nil
})
})
}
func writeToDos(w io.Writer, todos []ToDo) error {
for _, td := range todos {
if _, err := fmt.Fprintf(w, "%s (from %s)\n", td.message, td.provenance); err != nil {
return err
}
}
return nil
}