1
1
use std:: collections:: BTreeMap ;
2
+ use std:: collections:: HashMap ;
2
3
use std:: env;
3
4
4
5
use askama:: Template ;
@@ -8,8 +9,6 @@ use chrono::Duration;
8
9
use reqwest:: header:: HeaderMap ;
9
10
use serde_json as json;
10
11
11
- type JsonRefArray < ' a > = Vec < & ' a json:: Value > ;
12
-
13
12
const SKIP_LABELS : & [ & str ] = & [
14
13
"beta-nominated" ,
15
14
"beta-accepted" ,
@@ -18,6 +17,13 @@ const SKIP_LABELS: &[&str] = &[
18
17
"rollup" ,
19
18
] ;
20
19
20
+ const RELNOTES_LABELS : & [ & str ] = & [
21
+ "relnotes" ,
22
+ "relnotes-perf" ,
23
+ "finished-final-comment-period" ,
24
+ "needs-fcp" ,
25
+ ] ;
26
+
21
27
#[ derive( Clone , Template ) ]
22
28
#[ template( path = "relnotes.md" , escape = "none" ) ]
23
29
struct ReleaseNotes {
@@ -35,6 +41,8 @@ struct ReleaseNotes {
35
41
unsorted : String ,
36
42
unsorted_relnotes : String ,
37
43
version : String ,
44
+ internal_changes_relnotes : String ,
45
+ internal_changes_unsorted : String ,
38
46
}
39
47
40
48
fn main ( ) {
@@ -54,8 +62,8 @@ fn main() {
54
62
end = end + six_weeks;
55
63
}
56
64
57
- let mut issues = get_issues_by_milestone ( & version, "rust" ) ;
58
- issues . sort_by_cached_key ( |issue| issue [ "number" ] . as_u64 ( ) . unwrap ( ) ) ;
65
+ let issues = get_issues_by_milestone ( & version, "rust" ) ;
66
+ let mut tracking_rust = TrackingIssues :: collect ( & issues ) ;
59
67
60
68
// Skips `beta-accepted` as those PRs were backported onto the
61
69
// previous stable.
@@ -67,30 +75,75 @@ fn main() {
67
75
. any ( |o| SKIP_LABELS . contains ( & o[ "name" ] . as_str ( ) . unwrap ( ) ) )
68
76
} ) ;
69
77
70
- let relnotes_tags = & [ " relnotes" , "finished-final-comment-period" , "needs-fcp" ] ;
71
-
72
- let ( relnotes , rest ) = partition_by_tag ( in_release , relnotes_tags ) ;
78
+ let ( relnotes, rest ) = in_release
79
+ . into_iter ( )
80
+ . partition :: < Vec < _ > , _ > ( |o| has_tags ( o , RELNOTES_LABELS ) ) ;
73
81
74
82
let (
75
83
compat_relnotes,
76
84
libraries_relnotes,
77
85
language_relnotes,
78
86
compiler_relnotes,
87
+ internal_changes_relnotes,
79
88
unsorted_relnotes,
80
- ) = partition_prs ( relnotes) ;
89
+ ) = to_sections ( relnotes, & mut tracking_rust ) ;
81
90
82
- let ( compat_unsorted, libraries_unsorted, language_unsorted, compiler_unsorted, unsorted) =
83
- partition_prs ( rest) ;
91
+ let (
92
+ compat_unsorted,
93
+ libraries_unsorted,
94
+ language_unsorted,
95
+ compiler_unsorted,
96
+ internal_changes_unsorted,
97
+ unsorted,
98
+ ) = to_sections ( rest, & mut tracking_rust) ;
84
99
85
- let mut cargo_issues = get_issues_by_milestone ( & version, "cargo" ) ;
86
- cargo_issues. sort_by_cached_key ( |issue| issue[ "number" ] . as_u64 ( ) . unwrap ( ) ) ;
100
+ let cargo_issues = get_issues_by_milestone ( & version, "cargo" ) ;
87
101
88
102
let ( cargo_relnotes, cargo_unsorted) = {
89
- let ( relnotes, rest) = partition_by_tag ( cargo_issues. iter ( ) , relnotes_tags) ;
103
+ let ( relnotes, rest) = cargo_issues
104
+ . iter ( )
105
+ . partition :: < Vec < _ > , _ > ( |o| has_tags ( o, RELNOTES_LABELS ) ) ;
90
106
91
- ( map_to_line_items ( relnotes) , map_to_line_items ( rest) )
107
+ (
108
+ relnotes
109
+ . iter ( )
110
+ . map ( |o| {
111
+ format ! (
112
+ "- [{title}]({url}/)" ,
113
+ title = o[ "title" ] . as_str( ) . unwrap( ) ,
114
+ url = o[ "url" ] . as_str( ) . unwrap( ) ,
115
+ )
116
+ } )
117
+ . collect :: < Vec < _ > > ( )
118
+ . join ( "\n " ) ,
119
+ rest. iter ( )
120
+ . map ( |o| {
121
+ format ! (
122
+ "- [{title}]({url}/)" ,
123
+ title = o[ "title" ] . as_str( ) . unwrap( ) ,
124
+ url = o[ "url" ] . as_str( ) . unwrap( ) ,
125
+ )
126
+ } )
127
+ . collect :: < Vec < _ > > ( )
128
+ . join ( "\n " ) ,
129
+ )
92
130
} ;
93
131
132
+ for issue in tracking_rust. issues . values ( ) {
133
+ for ( section, ( used, _) ) in issue. sections . iter ( ) {
134
+ if * used {
135
+ continue ;
136
+ }
137
+
138
+ eprintln ! (
139
+ "Did not use {:?} from {} <{}>" ,
140
+ section,
141
+ issue. raw[ "title" ] . as_str( ) . unwrap( ) ,
142
+ issue. raw[ "url" ] . as_str( ) . unwrap( )
143
+ ) ;
144
+ }
145
+ }
146
+
94
147
let relnotes = ReleaseNotes {
95
148
version,
96
149
date : ( end + six_weeks) . naive_utc ( ) ,
@@ -104,6 +157,8 @@ fn main() {
104
157
compiler_unsorted,
105
158
cargo_relnotes,
106
159
cargo_unsorted,
160
+ internal_changes_relnotes,
161
+ internal_changes_unsorted,
107
162
unsorted_relnotes,
108
163
unsorted,
109
164
} ;
@@ -112,11 +167,29 @@ fn main() {
112
167
}
113
168
114
169
fn get_issues_by_milestone ( version : & str , repo_name : & ' static str ) -> Vec < json:: Value > {
170
+ let mut out = get_issues_by_milestone_inner ( version, repo_name, "issues" ) ;
171
+ out. extend ( get_issues_by_milestone_inner (
172
+ version,
173
+ repo_name,
174
+ "pullRequests" ,
175
+ ) ) ;
176
+ out. sort_unstable_by_key ( |v| v[ "number" ] . as_u64 ( ) . unwrap ( ) ) ;
177
+ out. dedup_by_key ( |v| v[ "number" ] . as_u64 ( ) . unwrap ( ) ) ;
178
+ out
179
+ }
180
+
181
+ fn get_issues_by_milestone_inner (
182
+ version : & str ,
183
+ repo_name : & ' static str ,
184
+ ty : & str ,
185
+ ) -> Vec < json:: Value > {
115
186
use reqwest:: blocking:: Client ;
116
187
117
188
let headers = request_header ( ) ;
118
189
let mut args = BTreeMap :: new ( ) ;
119
- args. insert ( "states" , String :: from ( "[MERGED]" ) ) ;
190
+ if ty == "pullRequests" {
191
+ args. insert ( "states" , String :: from ( "[MERGED]" ) ) ;
192
+ }
120
193
args. insert ( "last" , String :: from ( "100" ) ) ;
121
194
let mut issues = Vec :: new ( ) ;
122
195
@@ -128,11 +201,12 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
128
201
milestones(query: "{version}", first: 1) {{
129
202
totalCount
130
203
nodes {{
131
- pullRequests ({args}) {{
204
+ {ty} ({args}) {{
132
205
nodes {{
133
206
number
134
207
title
135
208
url
209
+ body
136
210
labels(last: 100) {{
137
211
nodes {{
138
212
name
@@ -149,6 +223,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
149
223
}}"# ,
150
224
repo_name = repo_name,
151
225
version = version,
226
+ ty = ty,
152
227
args = args
153
228
. iter( )
154
229
. map( |( k, v) | format!( "{}: {}" , k, v) )
@@ -170,9 +245,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
170
245
. send ( )
171
246
. unwrap ( ) ;
172
247
let status = response. status ( ) ;
173
- let json = response
174
- . json :: < json:: Value > ( )
175
- . unwrap ( ) ;
248
+ let json = response. json :: < json:: Value > ( ) . unwrap ( ) ;
176
249
if !status. is_success ( ) {
177
250
panic ! ( "API Error {}: {}" , status, json) ;
178
251
}
@@ -184,7 +257,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
184
257
"More than one milestone matched the query \" {version}\" . Please be more specific." ,
185
258
version = version
186
259
) ;
187
- let pull_requests_data = milestones_data[ "nodes" ] [ 0 ] [ "pullRequests" ] . clone ( ) ;
260
+ let pull_requests_data = milestones_data[ "nodes" ] [ 0 ] [ ty ] . clone ( ) ;
188
261
189
262
let mut pull_requests = pull_requests_data[ "nodes" ] . as_array ( ) . unwrap ( ) . clone ( ) ;
190
263
issues. append ( & mut pull_requests) ;
@@ -212,45 +285,146 @@ fn request_header() -> HeaderMap {
212
285
headers
213
286
}
214
287
215
- fn map_to_line_items < ' a > ( iter : impl IntoIterator < Item = & ' a json:: Value > ) -> String {
216
- iter. into_iter ( )
217
- . map ( |o| {
218
- format ! (
219
- "- [{title}]({url}/)" ,
220
- title = o[ "title" ] . as_str( ) . unwrap( ) ,
221
- url = o[ "url" ] . as_str( ) . unwrap( ) ,
222
- )
223
- } )
224
- . collect :: < Vec < _ > > ( )
225
- . join ( "\n " )
288
+ struct TrackingIssues {
289
+ // Maps the issue/PR number *tracked* by the issue in `json::Value`.
290
+ //
291
+ // bool is tracking whether we've used that issue already.
292
+ issues : HashMap < u64 , TrackingIssue > ,
226
293
}
227
294
228
- fn partition_by_tag < ' a > (
229
- iter : impl IntoIterator < Item = & ' a json:: Value > ,
230
- tags : & [ & str ] ,
231
- ) -> ( JsonRefArray < ' a > , JsonRefArray < ' a > ) {
232
- iter. into_iter ( ) . partition ( |o| {
233
- o[ "labels" ] [ "nodes" ]
234
- . as_array ( )
235
- . unwrap ( )
236
- . iter ( )
237
- . any ( |o| tags. iter ( ) . any ( |tag| o[ "name" ] == * tag) )
238
- } )
295
+ #[ derive( Debug ) ]
296
+ struct TrackingIssue {
297
+ raw : json:: Value ,
298
+ // Section name -> (used, lines)
299
+ sections : HashMap < String , ( bool , Vec < String > ) > ,
300
+ }
301
+
302
+ impl TrackingIssues {
303
+ fn collect ( all : & [ json:: Value ] ) -> Self {
304
+ let prefix = "Tracking issue for release notes of #" ;
305
+ let mut tracking_issues = HashMap :: new ( ) ;
306
+ for o in all. iter ( ) {
307
+ let title = o[ "title" ] . as_str ( ) . unwrap ( ) ;
308
+ if let Some ( tail) = title. strip_prefix ( prefix) {
309
+ let for_number = tail[ ..tail. find ( ':' ) . unwrap ( ) ] . parse :: < u64 > ( ) . unwrap ( ) ;
310
+ let mut sections = HashMap :: new ( ) ;
311
+ let body = o[ "body" ] . as_str ( ) . unwrap ( ) ;
312
+ let relnotes = body
313
+ . split ( "```" )
314
+ . nth ( 1 )
315
+ . unwrap ( )
316
+ . strip_prefix ( "markdown" )
317
+ . unwrap ( ) ;
318
+ let mut in_section = None ;
319
+ for line in relnotes. lines ( ) {
320
+ if line. trim ( ) . is_empty ( ) {
321
+ continue ;
322
+ }
323
+
324
+ if let Some ( header) = line. strip_prefix ( "# " ) {
325
+ in_section = Some ( header) ;
326
+ continue ;
327
+ }
328
+
329
+ if let Some ( section) = in_section {
330
+ sections
331
+ . entry ( section. to_owned ( ) )
332
+ . or_insert_with ( || ( false , vec ! [ ] ) )
333
+ . 1
334
+ . push ( line. to_owned ( ) ) ;
335
+ }
336
+ }
337
+ tracking_issues. insert (
338
+ for_number,
339
+ TrackingIssue {
340
+ raw : o. clone ( ) ,
341
+ sections,
342
+ } ,
343
+ ) ;
344
+ }
345
+ }
346
+ Self {
347
+ issues : tracking_issues,
348
+ }
349
+ }
239
350
}
240
351
241
- fn partition_prs < ' a > (
352
+ fn map_to_line_items < ' a > (
242
353
iter : impl IntoIterator < Item = & ' a json:: Value > ,
243
- ) -> ( String , String , String , String , String ) {
244
- let ( compat_notes, rest) = partition_by_tag ( iter, & [ "C-future-compatibility" ] ) ;
245
- let ( libs, rest) = partition_by_tag ( rest, & [ "T-libs" , "T-libs-api" ] ) ;
246
- let ( lang, rest) = partition_by_tag ( rest, & [ "T-lang" ] ) ;
247
- let ( compiler, rest) = partition_by_tag ( rest, & [ "T-compiler" ] ) ;
354
+ tracking_issues : & mut TrackingIssues ,
355
+ by_section : & mut HashMap < & ' static str , String > ,
356
+ ) {
357
+ for o in iter {
358
+ let title = o[ "title" ] . as_str ( ) . unwrap ( ) ;
359
+ if title. starts_with ( "Tracking issue for release notes of #" ) {
360
+ continue ;
361
+ }
362
+ let number = o[ "number" ] . as_u64 ( ) . unwrap ( ) ;
363
+
364
+ if let Some ( issue) = tracking_issues. issues . get_mut ( & number) {
365
+ for ( section, ( used, lines) ) in issue. sections . iter_mut ( ) {
366
+ if let Some ( contents) = by_section. get_mut ( section. as_str ( ) ) {
367
+ * used = true ;
368
+ for line in lines. iter ( ) {
369
+ contents. push_str ( line) ;
370
+ contents. push ( '\n' ) ;
371
+ }
372
+ }
373
+ }
248
374
375
+ // If we have a dedicated tracking issue, don't use our default rules.
376
+ continue ;
377
+ }
378
+
379
+ // In the future we expect to have increasingly few things fall into this category, as
380
+ // things are added to the dedicated tracking issue category in triagebot (today mostly
381
+ // FCPs are missing).
382
+
383
+ let section = if has_tags ( o, & [ "C-future-compatibility" ] ) {
384
+ "Compatibility Notes"
385
+ } else if has_tags ( o, & [ "T-libs" , "T-libs-api" ] ) {
386
+ "Library"
387
+ } else if has_tags ( o, & [ "T-lang" ] ) {
388
+ "Language"
389
+ } else if has_tags ( o, & [ "T-compiler" ] ) {
390
+ "Compiler"
391
+ } else {
392
+ "Other"
393
+ } ;
394
+ by_section. get_mut ( section) . unwrap ( ) . push_str ( & format ! (
395
+ "- [{title}]({url}/)\n " ,
396
+ title = o[ "title" ] . as_str( ) . unwrap( ) ,
397
+ url = o[ "url" ] . as_str( ) . unwrap( ) ,
398
+ ) ) ;
399
+ }
400
+ }
401
+
402
+ fn has_tags < ' a > ( o : & ' a json:: Value , tags : & [ & str ] ) -> bool {
403
+ o[ "labels" ] [ "nodes" ]
404
+ . as_array ( )
405
+ . unwrap ( )
406
+ . iter ( )
407
+ . any ( |o| tags. iter ( ) . any ( |tag| o[ "name" ] == * tag) )
408
+ }
409
+
410
+ fn to_sections < ' a > (
411
+ iter : impl IntoIterator < Item = & ' a json:: Value > ,
412
+ mut tracking : & mut TrackingIssues ,
413
+ ) -> ( String , String , String , String , String , String ) {
414
+ let mut by_section = HashMap :: new ( ) ;
415
+ by_section. insert ( "Compatibility Notes" , String :: new ( ) ) ;
416
+ by_section. insert ( "Library" , String :: new ( ) ) ;
417
+ by_section. insert ( "Language" , String :: new ( ) ) ;
418
+ by_section. insert ( "Compiler" , String :: new ( ) ) ;
419
+ by_section. insert ( "Internal Changes" , String :: new ( ) ) ;
420
+ by_section. insert ( "Other" , String :: new ( ) ) ;
421
+ map_to_line_items ( iter, & mut tracking, & mut by_section) ;
249
422
(
250
- map_to_line_items ( compat_notes) ,
251
- map_to_line_items ( libs) ,
252
- map_to_line_items ( lang) ,
253
- map_to_line_items ( compiler) ,
254
- map_to_line_items ( rest) ,
423
+ by_section. remove ( "Compatibility Notes" ) . unwrap ( ) ,
424
+ by_section. remove ( "Library" ) . unwrap ( ) ,
425
+ by_section. remove ( "Language" ) . unwrap ( ) ,
426
+ by_section. remove ( "Compiler" ) . unwrap ( ) ,
427
+ by_section. remove ( "Internal Changes" ) . unwrap ( ) ,
428
+ by_section. remove ( "Other" ) . unwrap ( ) ,
255
429
)
256
430
}
0 commit comments