@@ -14,13 +14,16 @@ import (
14
14
"html"
15
15
"io/ioutil"
16
16
"log"
17
+ "path"
17
18
"regexp"
18
19
"sort"
19
20
"strings"
20
21
"time"
21
22
23
+ "golang.org/x/build/gerrit"
22
24
"golang.org/x/build/maintner"
23
25
"golang.org/x/build/maintner/godata"
26
+ "golang.org/x/build/repos"
24
27
)
25
28
26
29
var (
@@ -55,6 +58,15 @@ func main() {
55
58
// Previous release was 6 months earlier.
56
59
cutoff = cutoff .AddDate (0 , - 6 , 0 )
57
60
61
+ // The maintner corpus doesn't track inline comments. See golang.org/issue/24863.
62
+ // So we need to use a Gerrit API client to fetch them instead. If maintner starts
63
+ // tracking inline comments in the future, this extra complexity can be dropped.
64
+ gerritClient := gerrit .NewClient ("https://go-review.googlesource.com" , gerrit .NoAuth )
65
+ matchedCLs , err := findCLsWithRelNote (gerritClient , cutoff )
66
+ if err != nil {
67
+ log .Fatal (err )
68
+ }
69
+
58
70
var existingHTML []byte
59
71
if * exclFile != "" {
60
72
var err error
@@ -68,21 +80,34 @@ func main() {
68
80
if err != nil {
69
81
log .Fatal (err )
70
82
}
71
- ger := corpus .Gerrit ()
72
83
changes := map [string ][]change {} // keyed by pkg
73
- ger .ForeachProjectUnsorted (func (gp * maintner.GerritProject ) error {
84
+ corpus . Gerrit () .ForeachProjectUnsorted (func (gp * maintner.GerritProject ) error {
74
85
if gp .Server () != "go.googlesource.com" {
75
86
return nil
76
87
}
77
88
gp .ForeachCLUnsorted (func (cl * maintner.GerritCL ) error {
78
89
if cl .Status != "merged" {
79
90
return nil
80
91
}
92
+ if cl .Branch () != "master" {
93
+ // Ignore CLs sent to development or release branches.
94
+ return nil
95
+ }
81
96
if cl .Commit .CommitTime .Before (cutoff ) {
82
97
// Was in a previous release; not for this one.
83
98
return nil
84
99
}
85
- relnote := clRelNote (cl )
100
+ _ , ok := matchedCLs [int (cl .Number )]
101
+ if ! ok {
102
+ // Wasn't matched by the Gerrit API search query.
103
+ // Return before making further Gerrit API calls.
104
+ return nil
105
+ }
106
+ comments , err := gerritClient .ListChangeComments (context .Background (), fmt .Sprint (cl .Number ))
107
+ if err != nil {
108
+ return err
109
+ }
110
+ relnote := clRelNote (cl , comments )
86
111
if relnote == "" ||
87
112
bytes .Contains (existingHTML , []byte (fmt .Sprintf ("CL %d" , cl .Number ))) {
88
113
return nil
@@ -119,7 +144,7 @@ func main() {
119
144
if strings .HasPrefix (pkg , "cmd/" ) {
120
145
continue
121
146
}
122
- fmt .Printf ("<dl id=%q><dt><a href=%q>%s</a></dt>\n <dd>" ,
147
+ fmt .Printf ("\n <dl id=%q><dt><a href=%q>%s</a></dt>\n <dd>" ,
123
148
pkg , "/pkg/" + pkg + "/" , pkg )
124
149
for _ , change := range changes [pkg ] {
125
150
changeURL := fmt .Sprintf ("https://golang.org/cl/%d" , change .CL .Number )
@@ -128,9 +153,8 @@ func main() {
128
153
fmt .Printf ("\n <p><!-- CL %d -->\n TODO: <a href=%q>%s</a>: %s\n </p>\n " ,
129
154
change .CL .Number , changeURL , changeURL , html .EscapeString (subj ))
130
155
}
131
- fmt .Printf (" </dd>\n </dl><!-- %s -->\n \n " , pkg )
156
+ fmt .Printf (" </dd>\n </dl><!-- %s -->\n " , pkg )
132
157
}
133
-
134
158
} else {
135
159
for _ , pkg := range pkgs {
136
160
fmt .Printf ("%s\n " , pkg )
@@ -141,34 +165,71 @@ func main() {
141
165
}
142
166
}
143
167
144
- // clPackage returns the package name from the CL's commit message,
145
- // or "??" if it's formatted unconventionally.
146
- func clPackage (cl * maintner.GerritCL ) string {
147
- subj := cl .Subject ()
148
- if i := strings .Index (subj , ":" ); i != - 1 {
149
- return subj [:i ]
168
+ // findCLsWithRelNote finds CLs that contain a RELNOTE marker by
169
+ // using a Gerrit API client. Returned map is keyed by CL number.
170
+ func findCLsWithRelNote (client * gerrit.Client , since time.Time ) (map [int ]* gerrit.ChangeInfo , error ) {
171
+ // Gerrit search operators are documented at
172
+ // https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators.
173
+ query := fmt .Sprintf (`status:merged branch:master since:%s (comment:"RELNOTE" OR comment:"RELNOTES")` ,
174
+ since .Format ("2006-01-02" ))
175
+ cs , err := client .QueryChanges (context .Background (), query )
176
+ if err != nil {
177
+ return nil , err
178
+ }
179
+ m := make (map [int ]* gerrit.ChangeInfo ) // CL Number → CL.
180
+ for _ , c := range cs {
181
+ m [c .ChangeNumber ] = c
150
182
}
151
- return "??"
183
+ return m , nil
152
184
}
153
185
154
- var relNoteRx = regexp .MustCompile (`RELNOTES?=(.+)` )
155
-
156
- func parseRelNote (s string ) string {
157
- if m := relNoteRx .FindStringSubmatch (s ); m != nil {
158
- return m [1 ]
186
+ // clPackage returns the package import path from the CL's commit message,
187
+ // or "??" if it's formatted unconventionally.
188
+ func clPackage (cl * maintner.GerritCL ) string {
189
+ var pkg string
190
+ if i := strings .Index (cl .Subject (), ":" ); i == - 1 {
191
+ return "??"
192
+ } else {
193
+ pkg = cl .Subject ()[:i ]
159
194
}
160
- return ""
195
+ if r := repos .ByGerritProject [cl .Project .Project ()]; r == nil {
196
+ return "??"
197
+ } else {
198
+ pkg = path .Join (r .ImportPath , pkg )
199
+ }
200
+ return pkg
161
201
}
162
202
163
- func clRelNote (cl * maintner.GerritCL ) string {
203
+ // clRelNote extracts a RELNOTE note from a Gerrit CL commit
204
+ // message and any inline comments. If there isn't a RELNOTE
205
+ // note, it returns the empty string.
206
+ func clRelNote (cl * maintner.GerritCL , comments map [string ][]gerrit.CommentInfo ) string {
164
207
msg := cl .Commit .Msg
165
208
if strings .Contains (msg , "RELNOTE" ) {
166
209
return parseRelNote (msg )
167
210
}
168
- for _ , comment := range cl .Messages {
169
- if strings .Contains (comment .Message , "RELNOTE" ) {
170
- return parseRelNote (comment .Message )
211
+ // Since July 2020, Gerrit UI has replaced top-level comments
212
+ // with patchset-level inline comments, so don't bother looking
213
+ // for RELNOTE= in cl.Messages—there won't be any. Instead, do
214
+ // look through all inline comments that we got via Gerrit API.
215
+ for _ , cs := range comments {
216
+ for _ , c := range cs {
217
+ if strings .Contains (c .Message , "RELNOTE" ) {
218
+ return parseRelNote (c .Message )
219
+ }
171
220
}
172
221
}
173
222
return ""
174
223
}
224
+
225
+ // parseRelNote parses a RELNOTE annotation from the string s.
226
+ // It returns the empty string if no such annotation exists.
227
+ func parseRelNote (s string ) string {
228
+ m := relNoteRx .FindStringSubmatch (s )
229
+ if m == nil {
230
+ return ""
231
+ }
232
+ return m [1 ]
233
+ }
234
+
235
+ var relNoteRx = regexp .MustCompile (`RELNOTES?=(.+)` )
0 commit comments