Skip to content

Commit f5330cc

Browse files
committed
cmd/relnote: look for CLs that refer to proposals
Add a TODO for every merged CL that mentions an accepted proposal. Also, move some functions from relnote.go to todo.go, where they are used. For golang/go#64169. Change-Id: I023dab2bb9627be35c1b4ab90fe5dd597b6fc9ca Reviewed-on: https://go-review.googlesource.com/c/build/+/584396 Reviewed-by: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 9c8263e commit f5330cc

File tree

2 files changed

+141
-103
lines changed

2 files changed

+141
-103
lines changed

cmd/relnote/relnote.go

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package main
99

1010
import (
11-
"context"
1211
"flag"
1312
"fmt"
1413
"log"
@@ -17,12 +16,9 @@ import (
1716
"path/filepath"
1817
"regexp"
1918
"runtime"
20-
"slices"
21-
"strconv"
2219
"strings"
2320
"time"
2421

25-
"golang.org/x/build/gerrit"
2622
"golang.org/x/build/maintner"
2723
"golang.org/x/build/repos"
2824
)
@@ -146,24 +142,6 @@ func main() {
146142
}
147143
}
148144

149-
// findCLsWithRelNote finds CLs that contain a RELNOTE marker by
150-
// using a Gerrit API client. Returned map is keyed by CL number.
151-
func findCLsWithRelNote(client *gerrit.Client, since time.Time) (map[int]*gerrit.ChangeInfo, error) {
152-
// Gerrit search operators are documented at
153-
// https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators.
154-
query := fmt.Sprintf(`status:merged branch:master since:%s (comment:"RELNOTE" OR comment:"RELNOTES")`,
155-
since.Format("2006-01-02"))
156-
cs, err := client.QueryChanges(context.Background(), query)
157-
if err != nil {
158-
return nil, err
159-
}
160-
m := make(map[int]*gerrit.ChangeInfo) // CL Number → CL.
161-
for _, c := range cs {
162-
m[c.ChangeNumber] = c
163-
}
164-
return m, nil
165-
}
166-
167145
// packagePrefix returns the package prefix at the start of s.
168146
// For example packagePrefix("net/http: add HTTP 5 support") == "net/http".
169147
// If there's no package prefix, packagePrefix returns "".
@@ -194,40 +172,6 @@ func clPackage(cl *maintner.GerritCL) string {
194172
return pkg
195173
}
196174

197-
// clRelNote extracts a RELNOTE note from a Gerrit CL commit
198-
// message and any inline comments. If there isn't a RELNOTE
199-
// note, it returns the empty string.
200-
func clRelNote(cl *maintner.GerritCL, comments map[string][]gerrit.CommentInfo) string {
201-
msg := cl.Commit.Msg
202-
if strings.Contains(msg, "RELNOTE") {
203-
return parseRelNote(msg)
204-
}
205-
// Since July 2020, Gerrit UI has replaced top-level comments
206-
// with patchset-level inline comments, so don't bother looking
207-
// for RELNOTE= in cl.Messages—there won't be any. Instead, do
208-
// look through all inline comments that we got via Gerrit API.
209-
for _, cs := range comments {
210-
for _, c := range cs {
211-
if strings.Contains(c.Message, "RELNOTE") {
212-
return parseRelNote(c.Message)
213-
}
214-
}
215-
}
216-
return ""
217-
}
218-
219-
// parseRelNote parses a RELNOTE annotation from the string s.
220-
// It returns the empty string if no such annotation exists.
221-
func parseRelNote(s string) string {
222-
m := relNoteRx.FindStringSubmatch(s)
223-
if m == nil {
224-
return ""
225-
}
226-
return m[1]
227-
}
228-
229-
var relNoteRx = regexp.MustCompile(`RELNOTES?=(.+)`)
230-
231175
// issuePackage returns the package import path from the issue's title,
232176
// or "??" if it's formatted unconventionally.
233177
func issuePackage(issue *maintner.GitHubIssue) string {
@@ -246,35 +190,3 @@ func issueSubject(issue *maintner.GitHubIssue) string {
246190
}
247191
return strings.TrimSpace(strings.TrimPrefix(issue.Title, pkg+":"))
248192
}
249-
250-
func hasLabel(issue *maintner.GitHubIssue, label string) bool {
251-
for _, l := range issue.Labels {
252-
if l.Name == label {
253-
return true
254-
}
255-
}
256-
return false
257-
}
258-
259-
var numbersRE = regexp.MustCompile(`(?m)(?:^|\s|golang/go)#([0-9]{3,})`)
260-
var golangGoNumbersRE = regexp.MustCompile(`(?m)golang/go#([0-9]{3,})`)
261-
262-
// issueNumbers returns the golang/go issue numbers referred to by the CL.
263-
func issueNumbers(cl *maintner.GerritCL) []int32 {
264-
var re *regexp.Regexp
265-
if cl.Project.Project() == "go" {
266-
re = numbersRE
267-
} else {
268-
re = golangGoNumbersRE
269-
}
270-
271-
var list []int32
272-
for _, s := range re.FindAllStringSubmatch(cl.Commit.Msg, -1) {
273-
if n, err := strconv.Atoi(s[1]); err == nil && n < 1e9 {
274-
list = append(list, int32(n))
275-
}
276-
}
277-
// Remove duplicates.
278-
slices.Sort(list)
279-
return slices.Compact(list)
280-
}

cmd/relnote/todo.go

Lines changed: 141 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import (
1010
"fmt"
1111
"io"
1212
"io/fs"
13+
"regexp"
14+
"slices"
15+
"strconv"
1316
"strings"
1417
"time"
1518

1619
"golang.org/x/build/gerrit"
1720
"golang.org/x/build/maintner"
1821
"golang.org/x/build/maintner/godata"
22+
"golang.org/x/exp/maps"
1923
)
2024

2125
type ToDo struct {
@@ -34,7 +38,7 @@ func todo(w io.Writer, fsys fs.FS, prevRelDate time.Time) error {
3438
return err
3539
}
3640
if !prevRelDate.IsZero() {
37-
if err := todosFromRelnoteCLs(prevRelDate, add); err != nil {
41+
if err := todosFromCLs(prevRelDate, add); err != nil {
3842
return err
3943
}
4044
}
@@ -77,7 +81,7 @@ func todosFromFile(dir fs.FS, filename string, add func(ToDo)) error {
7781
return scan.Err()
7882
}
7983

80-
func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error {
84+
func todosFromCLs(cutoff time.Time, add func(ToDo)) error {
8185
ctx := context.Background()
8286
// The maintner corpus doesn't track inline comments. See go.dev/issue/24863.
8387
// So we need to use a Gerrit API client to fetch them instead. If maintner starts
@@ -91,6 +95,7 @@ func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error {
9195
if err != nil {
9296
return err
9397
}
98+
gh := corpus.GitHub().Repo("golang", "go")
9499
return corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
95100
if gp.Server() != "go.googlesource.com" {
96101
return nil
@@ -107,30 +112,151 @@ func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error {
107112
// Was in a previous release; not for this one.
108113
return nil
109114
}
110-
// TODO(jba): look for accepted proposals that don't have release notes.
115+
// Add a TODO if the CL has a "RELNOTE=" comment.
116+
// These are deprecated, but we look for them just in case.
111117
if _, ok := matchedCLs[int(cl.Number)]; ok {
112-
comments, err := gerritClient.ListChangeComments(context.Background(), fmt.Sprint(cl.Number))
113-
if err != nil {
118+
if err := todoFromRelnote(ctx, cl, gerritClient, add); err != nil {
114119
return err
115120
}
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-
}
125121
}
122+
// Add a TODO if the CL refers to an accepted proposal.
123+
todoFromProposal(ctx, cl, gh, add)
126124
return nil
127125
})
128126
})
129127
}
130128

129+
func todoFromRelnote(ctx context.Context, cl *maintner.GerritCL, gc *gerrit.Client, add func(ToDo)) error {
130+
comments, err := gc.ListChangeComments(ctx, fmt.Sprint(cl.Number))
131+
if err != nil {
132+
return err
133+
}
134+
if rn := clRelNote(cl, comments); rn != "" {
135+
if rn == "yes" || rn == "y" {
136+
rn = "UNKNOWN"
137+
}
138+
add(ToDo{
139+
message: "TODO:" + rn,
140+
provenance: fmt.Sprintf("RELNOTE comment in https://go.dev/cl/%d", cl.Number),
141+
})
142+
}
143+
return nil
144+
}
145+
146+
func todoFromProposal(ctx context.Context, cl *maintner.GerritCL, gh *maintner.GitHubRepo, add func(ToDo)) {
147+
for _, num := range issueNumbers(cl) {
148+
// TODO(jba): look for CL references in existing release notes to avoid adding TODOs for
149+
// CLs that have already been documented.
150+
if issue := gh.Issue(num); issue != nil && hasLabel(issue, "Proposal-Accepted") {
151+
// Add a TODO for all issues, regardless of when or whether they are closed.
152+
// Any work on an accepted proposal is potentially worthy of a release note.
153+
add(ToDo{
154+
message: fmt.Sprintf("TODO: accepted proposal https://go.dev/issue/%d", num),
155+
provenance: fmt.Sprintf("https://go.dev/cl/%d", cl.Number),
156+
})
157+
}
158+
}
159+
}
160+
161+
func hasLabel(issue *maintner.GitHubIssue, label string) bool {
162+
for _, l := range issue.Labels {
163+
if l.Name == label {
164+
return true
165+
}
166+
}
167+
return false
168+
}
169+
170+
// findCLsWithRelNote finds CLs that contain a RELNOTE marker by
171+
// using a Gerrit API client. Returned map is keyed by CL number.
172+
func findCLsWithRelNote(client *gerrit.Client, since time.Time) (map[int]*gerrit.ChangeInfo, error) {
173+
// Gerrit search operators are documented at
174+
// https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators.
175+
query := fmt.Sprintf(`status:merged branch:master since:%s (comment:"RELNOTE" OR comment:"RELNOTES")`,
176+
since.Format("2006-01-02"))
177+
cs, err := client.QueryChanges(context.Background(), query)
178+
if err != nil {
179+
return nil, err
180+
}
181+
m := make(map[int]*gerrit.ChangeInfo) // CL Number → CL.
182+
for _, c := range cs {
183+
m[c.ChangeNumber] = c
184+
}
185+
return m, nil
186+
}
187+
188+
// clRelNote extracts a RELNOTE note from a Gerrit CL commit
189+
// message and any inline comments. If there isn't a RELNOTE
190+
// note, it returns the empty string.
191+
func clRelNote(cl *maintner.GerritCL, comments map[string][]gerrit.CommentInfo) string {
192+
msg := cl.Commit.Msg
193+
if strings.Contains(msg, "RELNOTE") {
194+
return parseRelNote(msg)
195+
}
196+
// Since July 2020, Gerrit UI has replaced top-level comments
197+
// with patchset-level inline comments, so don't bother looking
198+
// for RELNOTE= in cl.Messages—there won't be any. Instead, do
199+
// look through all inline comments that we got via Gerrit API.
200+
for _, cs := range comments {
201+
for _, c := range cs {
202+
if strings.Contains(c.Message, "RELNOTE") {
203+
return parseRelNote(c.Message)
204+
}
205+
}
206+
}
207+
return ""
208+
}
209+
210+
var relNoteRx = regexp.MustCompile(`RELNOTES?=(.+)`)
211+
212+
// parseRelNote parses a RELNOTE annotation from the string s.
213+
// It returns the empty string if no such annotation exists.
214+
func parseRelNote(s string) string {
215+
m := relNoteRx.FindStringSubmatch(s)
216+
if m == nil {
217+
return ""
218+
}
219+
return m[1]
220+
}
221+
222+
var numbersRE = regexp.MustCompile(`(?m)(?:^|\s|golang/go)#([0-9]{3,})`)
223+
var golangGoNumbersRE = regexp.MustCompile(`(?m)golang/go#([0-9]{3,})`)
224+
225+
// issueNumbers returns the golang/go issue numbers referred to by the CL.
226+
func issueNumbers(cl *maintner.GerritCL) []int32 {
227+
var re *regexp.Regexp
228+
if cl.Project.Project() == "go" {
229+
re = numbersRE
230+
} else {
231+
re = golangGoNumbersRE
232+
}
233+
234+
var list []int32
235+
for _, s := range re.FindAllStringSubmatch(cl.Commit.Msg, -1) {
236+
if n, err := strconv.Atoi(s[1]); err == nil && n < 1e9 {
237+
list = append(list, int32(n))
238+
}
239+
}
240+
// Remove duplicates.
241+
slices.Sort(list)
242+
return slices.Compact(list)
243+
}
244+
131245
func writeToDos(w io.Writer, todos []ToDo) error {
246+
// Group TODOs with the same message. This simplifies the output when a single
247+
// issue is implemented by multiple CLs.
248+
byMessage := map[string][]ToDo{}
132249
for _, td := range todos {
133-
if _, err := fmt.Fprintf(w, "%s (from %s)\n", td.message, td.provenance); err != nil {
250+
byMessage[td.message] = append(byMessage[td.message], td)
251+
}
252+
msgs := maps.Keys(byMessage)
253+
slices.Sort(msgs) // for deterministic output
254+
for _, msg := range msgs {
255+
var provs []string
256+
for _, td := range byMessage[msg] {
257+
provs = append(provs, td.provenance)
258+
}
259+
if _, err := fmt.Fprintf(w, "%s (from %s)\n", msg, strings.Join(provs, ", ")); err != nil {
134260
return err
135261
}
136262
}

0 commit comments

Comments
 (0)