@@ -9,10 +9,13 @@ package main
9
9
import (
10
10
"bytes"
11
11
"context"
12
+ "encoding/json"
12
13
"flag"
13
14
"fmt"
14
15
"io/ioutil"
15
16
"log"
17
+ "net/http"
18
+
16
19
"path/filepath"
17
20
"regexp"
18
21
"sort"
@@ -27,17 +30,26 @@ import (
27
30
)
28
31
29
32
var (
30
- milestone = flag .String ("milestone" , "" , "milestone associated with the release" )
31
- filterDirs = flag .String ("dirs" , "" , "comma-separated list of directories that should be touched for a CL to be considered relevant" )
32
- sinceCL = flag .Int ("cl" , - 1 , "the gerrit change number of the first CL to include in the output. Only changes submitted more recently than 'cl' will be included." )
33
- project = flag .String ("project" , "vscode-go" , "name of the golang project" )
34
- mdMode = flag .Bool ("md" , false , "write MD output" )
35
- exclFile = flag .String ("exclude-from" , "" , "optional path to changelog MD file. If specified, any 'CL NNNN' occurence in the content will cause that CL to be excluded from this tool's output." )
33
+ milestone = flag .String ("milestone" , "" , "milestone associated with the release" )
34
+ filterDirs = flag .String ("dirs" , "" , "comma-separated list of directories that should be touched for a CL to be considered relevant" )
35
+ sinceCL = flag .Int ("cl" , - 1 , "the gerrit change number of the first CL to include in the output. Only changes submitted more recently than 'cl' will be included." )
36
+ project = flag .String ("project" , "vscode-go" , "name of the golang project" )
37
+ exclFile = flag .String ("exclude-from" , "" , "optional path to changelog MD file. If specified, any 'CL NNNN' occurence in the content will cause that CL to be excluded from this tool's output." )
38
+ semanticVersion = flag .String ("semver" , "" , "the semantic version of the new release" )
39
+ githubTokenFilePath = flag .String ("token" , "" , "the absolute path to the github token file" )
36
40
)
37
41
38
42
func main () {
39
43
flag .Parse ()
40
44
45
+ if * semanticVersion == "" {
46
+ log .Fatal ("Must provide -semver." )
47
+ }
48
+
49
+ if * githubTokenFilePath == "" {
50
+ log .Fatal ("Must provide -token." )
51
+ }
52
+
41
53
var existingMD []byte
42
54
if * exclFile != "" {
43
55
var err error
@@ -86,7 +98,7 @@ func main() {
86
98
})
87
99
88
100
var changes []* generic.Changelist
89
- authors := map [* maintner.GitPerson ]bool {}
101
+ cls := map [* maintner.GerritCL ]bool {}
90
102
ger .ForeachProjectUnsorted (func (gp * maintner.GerritProject ) error {
91
103
if gp .Server () != "go.googlesource.com" || gp .Project () != * project {
92
104
return nil
@@ -124,34 +136,50 @@ func main() {
124
136
return nil
125
137
}
126
138
}
127
- changes = append (changes , golang .GerritToGenericCL (cl ))
128
- authors [cl .Owner ()] = true
139
+ if isGoplsChangeList (golang .GerritToGenericCL (cl )) {
140
+ changes = append (changes , golang .GerritToGenericCL (cl ))
141
+ cls [cl ] = true
142
+ }
129
143
return nil
130
144
})
131
145
return nil
132
146
})
133
147
134
- sort .Slice (changes , func (i , j int ) bool {
135
- return changes [i ].Number < changes [j ].Number
136
- })
148
+ fmt .Printf ("# Version: %s\n \n " , * semanticVersion )
149
+ fmt .Printf ("## TODO: version - " )
150
+ now := time .Now ()
151
+ fmt .Printf ("%s\n \n " , now .Format ("2 Jan, 2006" ))
152
+ fmt .Printf ("### Changes\n \n " )
153
+ mdPrintChanges (changes , false )
154
+ fmt .Printf ("\n \n " )
137
155
138
- if * mdMode {
139
- fmt .Printf ("## TODO: version - " )
140
- now := time .Now ()
141
- fmt .Printf ("%s\n \n " , now .Format ("2 Jan, 2006" ))
142
- fmt .Printf ("### Changes\n \n " )
143
- mdPrintChanges (changes , true )
156
+ fmt .Printf ("### Issues\n \n " )
157
+ mdPrintIssues (changes , * milestone )
158
+ fmt .Printf ("\n \n " )
144
159
145
- fmt .Printf ("### Issues\n \n " )
146
- mdPrintIssues (changes , * milestone )
160
+ fmt .Printf ("### Release comments\n \n " )
161
+ mdPrintReleaseComments (changes )
162
+ fmt .Printf ("\n \n " )
147
163
148
- fmt .Printf ("\n ### Thanks\n \n " )
149
- mdPrintContributors (authors )
150
- } else {
151
- for _ , change := range changes {
152
- fmt .Printf (" %s\n " , change .Subject )
164
+ fmt .Printf ("\n ### Thanks\n \n " )
165
+ mdPrintContributors (cls )
166
+ }
167
+
168
+ func isGoplsChangeList (cl * generic.Changelist ) bool {
169
+ if strings .Contains (cl .Subject , "internal/lsp" ) || strings .Contains (cl .Subject , "gopls" ) {
170
+ return true
171
+ }
172
+ for _ , issue := range cl .AssociatedIssues {
173
+ if issue .Repo == "golang/vscode-go" {
174
+ return true
175
+ }
176
+ for _ , label := range issue .Labels {
177
+ if label == "gopls" {
178
+ return true
179
+ }
153
180
}
154
181
}
182
+ return false
155
183
}
156
184
157
185
func mdPrintChanges (changes []* generic.Changelist , byCategory bool ) {
@@ -178,7 +206,7 @@ func mdPrintChanges(changes []*generic.Changelist, byCategory bool) {
178
206
}
179
207
fmt .Printf (" <!-- CL %d -->\n " , change .Number )
180
208
}
181
- // Group CLs by category or by number order .
209
+ // Group CLs by category or by first associated issue number .
182
210
if byCategory {
183
211
pkgMap := map [string ][]* generic.Changelist {}
184
212
for _ , change := range changes {
@@ -190,7 +218,29 @@ func mdPrintChanges(changes []*generic.Changelist, byCategory bool) {
190
218
}
191
219
}
192
220
} else {
193
- for _ , change := range changes {
221
+ sort .Slice (changes , func (i , j int ) bool {
222
+ // Sort first by associated issue, then by CL number.
223
+ var iIssue , jIssue int // first associated issues
224
+ if len (changes [i ].AssociatedIssues ) > 0 {
225
+ iIssue = changes [i ].AssociatedIssues [0 ].Number
226
+ }
227
+ if len (changes [j ].AssociatedIssues ) > 0 {
228
+ jIssue = changes [j ].AssociatedIssues [0 ].Number
229
+ }
230
+ if iIssue != 0 && jIssue != 0 {
231
+ return iIssue < jIssue // sort CLs with issues first
232
+ }
233
+ return iIssue != 0 || changes [i ].Number < changes [j ].Number
234
+ })
235
+
236
+ currentChange := - 1
237
+ for i , change := range changes {
238
+ if len (change .AssociatedIssues ) > 0 && change .AssociatedIssues [0 ].Number != currentChange {
239
+ currentChange = change .AssociatedIssues [0 ].Number
240
+ fmt .Printf ("CL(s) for issue %d:\n " , currentChange )
241
+ } else if len (change .AssociatedIssues ) == 0 && (i == 0 || len (changes [i - 1 ].AssociatedIssues ) > 0 ) {
242
+ fmt .Printf ("CL(s) not associated with any issue:\n " )
243
+ }
194
244
printChange (change )
195
245
}
196
246
}
@@ -212,6 +262,22 @@ func mdPrintIssues(changes []*generic.Changelist, milestone string) {
212
262
}
213
263
}
214
264
265
+ func mdPrintReleaseComments (changes []* generic.Changelist ) {
266
+ type Issue struct {
267
+ repo string
268
+ number int
269
+ }
270
+ printedIssues := make (map [Issue ]bool )
271
+ for _ , change := range changes {
272
+ for _ , issue := range change .AssociatedIssues {
273
+ if _ , ok := printedIssues [Issue {issue .Repo , issue .Number }]; ! ok {
274
+ printedIssues [Issue {issue .Repo , issue .Number }] = true
275
+ printIssueReleaseComment (issue .Repo , issue .Number )
276
+ }
277
+ }
278
+ }
279
+ }
280
+
215
281
// clPackage returns the package name from the CL's commit message,
216
282
// or "??" if it's formatted unconventionally.
217
283
func clPackage (cl * maintner.GerritCL ) string {
@@ -243,17 +309,90 @@ func releaseNote(cl *generic.Changelist) string {
243
309
return ""
244
310
}
245
311
246
- func mdPrintContributors (authors map [* maintner.GitPerson ]bool ) {
247
- var names []string
248
- for author := range authors {
249
- // It would be great to look up the GitHub username by using:
250
- // https://pkg.go.dev/golang.org/x/build/internal/gophers#GetPerson.
251
- names = append (names , author .Name ())
312
+ func mdPrintContributors (cls map [* maintner.GerritCL ]bool ) {
313
+ var usernames []string
314
+ for changelist := range cls {
315
+ author , err := fetchCLAuthorName (changelist , * project )
316
+ if err != nil {
317
+ log .Fatal ("Error fetching Github information for %s: %v\n " , changelist .Owner (), err )
318
+ }
319
+ usernames = append (usernames , author )
320
+ }
321
+ usernames = unique (usernames )
322
+ if len (usernames ) > 1 {
323
+ usernames [len (usernames )- 1 ] = "and " + usernames [len (usernames )- 1 ]
324
+ }
325
+
326
+ fmt .Printf ("Thank you for your contribution, %s!\n " , strings .Join (usernames , ", " ))
327
+ }
328
+
329
+ func getURL (url string ) ([]byte , error ) {
330
+ req , _ := http .NewRequest ("GET" , url , nil )
331
+ if token , err := ioutil .ReadFile (* githubTokenFilePath ); err == nil {
332
+ req .Header .Set ("Authorization" , "token " + strings .TrimSpace (string (token )))
333
+ }
334
+ res , err := http .DefaultClient .Do (req )
335
+ if err != nil {
336
+ return nil , err
337
+ }
338
+ defer res .Body .Close ()
339
+ body , err := ioutil .ReadAll (res .Body )
340
+ if err != nil {
341
+ log .Fatalf ("Error fetching Github information at %s: %v\n " , url , err )
342
+ }
343
+ return body , nil
344
+ }
345
+
346
+ func fetchCLAuthorName (changelist * maintner.GerritCL , repo string ) (string , error ) {
347
+ githubRepoMapping := map [string ]string {
348
+ "tools" : "golang/tools" ,
349
+ "vscode-go" : "golang/vscode-go" ,
350
+ }
351
+ body , err := getURL (fmt .Sprintf ("https://api.github.com/repos/%s/commits/%s" , githubRepoMapping [repo ], changelist .Commit .Hash ))
352
+ if err != nil {
353
+ return "" , err
354
+ }
355
+ var resp map [string ]interface {}
356
+ if err := json .Unmarshal (body , & resp ); err != nil {
357
+ return "" , err
358
+ }
359
+ if authorInfo , _ := resp ["author" ].(map [string ]interface {}); authorInfo != nil {
360
+ if username , ok := authorInfo ["login" ].(string ); ok {
361
+ return "@" + username , nil
362
+ }
363
+ }
364
+ return changelist .Owner ().Name (), nil
365
+ }
366
+
367
+ // printIssueReleaseComment collects the release comments, which marked by the annotation *Release*, from the issues included in this release.
368
+ func printIssueReleaseComment (repo string , issueNumber int ) {
369
+ body , err := getURL (fmt .Sprintf ("https://api.github.com/repos/%s/issues/%d/comments" , repo , issueNumber ))
370
+ if err != nil {
371
+ log .Fatal (err )
252
372
}
253
- sort .Strings (names )
254
- if len (names ) > 1 {
255
- names [len (names )- 1 ] = "and " + names [len (names )- 1 ]
373
+ var issueComments []interface {}
374
+ if err := json .Unmarshal (body , & issueComments ); err != nil {
375
+ log .Fatalf ("Error fetching Github information for issue %d:\n " , issueNumber )
376
+ }
377
+ for _ , comment := range issueComments {
378
+ c , _ := comment .(map [string ]interface {})
379
+ if str , ok := c ["body" ].(string ); ok && strings .Contains (str , "*Release*" ) {
380
+ fmt .Println (str )
381
+ return
382
+ }
256
383
}
384
+ }
257
385
258
- fmt .Printf ("Thank you for your contribution, %s!\n " , strings .Join (names , ", " ))
386
+ // unique returns a ascendingly sorted set of unique strings among its input
387
+ func unique (input []string ) []string {
388
+ m := make (map [string ]bool )
389
+ for _ , entry := range input {
390
+ m [entry ] = true
391
+ }
392
+ var list []string
393
+ for key , _ := range m {
394
+ list = append (list , key )
395
+ }
396
+ sort .Strings (list )
397
+ return list
259
398
}
0 commit comments