golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/relnote/todo.go (about) 1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bufio" 9 "context" 10 "fmt" 11 "io" 12 "io/fs" 13 "strings" 14 "time" 15 16 "golang.org/x/build/gerrit" 17 "golang.org/x/build/maintner" 18 "golang.org/x/build/maintner/godata" 19 ) 20 21 type ToDo struct { 22 message string // what is to be done 23 provenance string // where the TODO came from 24 } 25 26 // todo prints a report to w on which release notes need to be written. 27 // It takes the doc/next directory of the repo and the date of the last release. 28 func todo(w io.Writer, fsys fs.FS, prevRelDate time.Time) error { 29 var todos []ToDo 30 31 add := func(td ToDo) { todos = append(todos, td) } 32 33 if err := todosFromDocFiles(fsys, add); err != nil { 34 return err 35 } 36 if !prevRelDate.IsZero() { 37 if err := todosFromRelnoteCLs(prevRelDate, add); err != nil { 38 return err 39 } 40 } 41 return writeToDos(w, todos) 42 } 43 44 // Collect TODOs from the markdown files in the main repo. 45 func todosFromDocFiles(fsys fs.FS, add func(ToDo)) error { 46 // This is essentially a grep. 47 return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { 48 if err != nil { 49 return err 50 } 51 if !d.IsDir() && strings.HasSuffix(path, ".md") { 52 if err := todosFromFile(fsys, path, add); err != nil { 53 return err 54 } 55 } 56 return nil 57 }) 58 } 59 60 func todosFromFile(dir fs.FS, filename string, add func(ToDo)) error { 61 f, err := dir.Open(filename) 62 if err != nil { 63 return err 64 } 65 defer f.Close() 66 scan := bufio.NewScanner(f) 67 ln := 0 68 for scan.Scan() { 69 ln++ 70 if line := scan.Text(); strings.Contains(line, "TODO") { 71 add(ToDo{ 72 message: line, 73 provenance: fmt.Sprintf("%s:%d", filename, ln), 74 }) 75 } 76 } 77 return scan.Err() 78 } 79 80 func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error { 81 ctx := context.Background() 82 // The maintner corpus doesn't track inline comments. See go.dev/issue/24863. 83 // So we need to use a Gerrit API client to fetch them instead. If maintner starts 84 // tracking inline comments in the future, this extra complexity can be dropped. 85 gerritClient := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth) 86 matchedCLs, err := findCLsWithRelNote(gerritClient, cutoff) 87 if err != nil { 88 return err 89 } 90 corpus, err := godata.Get(ctx) 91 if err != nil { 92 return err 93 } 94 return corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error { 95 if gp.Server() != "go.googlesource.com" { 96 return nil 97 } 98 return gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error { 99 if cl.Status != "merged" { 100 return nil 101 } 102 if cl.Branch() != "master" { 103 // Ignore CLs sent to development or release branches. 104 return nil 105 } 106 if cl.Commit.CommitTime.Before(cutoff) { 107 // Was in a previous release; not for this one. 108 return nil 109 } 110 // TODO(jba): look for accepted proposals that don't have release notes. 111 if _, ok := matchedCLs[int(cl.Number)]; ok { 112 comments, err := gerritClient.ListChangeComments(context.Background(), fmt.Sprint(cl.Number)) 113 if err != nil { 114 return err 115 } 116 if rn := clRelNote(cl, comments); rn != "" { 117 if rn == "yes" || rn == "y" { 118 rn = "UNKNOWN" 119 } 120 add(ToDo{ 121 message: "TODO:" + rn, 122 provenance: fmt.Sprintf("RELNOTE comment in https://go.dev/cl/%d", cl.Number), 123 }) 124 } 125 } 126 return nil 127 }) 128 }) 129 } 130 131 func writeToDos(w io.Writer, todos []ToDo) error { 132 for _, td := range todos { 133 if _, err := fmt.Fprintf(w, "%s (from %s)\n", td.message, td.provenance); err != nil { 134 return err 135 } 136 } 137 return nil 138 }