1
1
use std:: {
2
- collections:: BTreeSet ,
2
+ collections:: { BTreeMap , BTreeSet } ,
3
3
fmt:: Display ,
4
4
path:: { Path , PathBuf } ,
5
5
process:: Command ,
@@ -152,15 +152,26 @@ pub fn generate_issues(
152
152
#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
153
153
pub struct GithubIssue {
154
154
pub title : String ,
155
- pub assignees : Vec < String > ,
155
+ pub assignees : BTreeSet < String > ,
156
156
pub body : String ,
157
157
pub labels : Vec < String > ,
158
158
}
159
159
160
160
#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
161
161
enum GithubAction {
162
- CreateLabel { label : GhLabel } ,
163
- CreateIssue { issue : GithubIssue } ,
162
+ CreateLabel {
163
+ label : GhLabel ,
164
+ } ,
165
+ CreateIssue {
166
+ issue : GithubIssue ,
167
+ } ,
168
+
169
+ // We intentionally do not sync the issue *text*, because it may have been edited.
170
+ SyncIssue {
171
+ number : u64 ,
172
+ remove_owners : BTreeSet < String > ,
173
+ add_owners : BTreeSet < String > ,
174
+ } ,
164
175
}
165
176
166
177
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -171,7 +182,21 @@ struct GhLabel {
171
182
172
183
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
173
184
struct ExistingGithubIssue {
185
+ number : u64 ,
186
+ assignees : BTreeSet < String > ,
187
+ }
188
+
189
+ #[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
190
+ struct ExistingGithubIssueJson {
174
191
title : String ,
192
+ number : u64 ,
193
+ assignees : Vec < ExistingGithubAssigneeJson > ,
194
+ }
195
+
196
+ #[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
197
+ struct ExistingGithubAssigneeJson {
198
+ login : String ,
199
+ name : String ,
175
200
}
176
201
177
202
fn list_labels ( repository : & str ) -> anyhow:: Result < Vec < GhLabel > > {
@@ -192,7 +217,7 @@ fn list_labels(repository: &str) -> anyhow::Result<Vec<GhLabel>> {
192
217
fn list_issue_titles_in_milestone (
193
218
repository : & str ,
194
219
timeframe : & str ,
195
- ) -> anyhow:: Result < BTreeSet < String > > {
220
+ ) -> anyhow:: Result < BTreeMap < String , ExistingGithubIssue > > {
196
221
let output = Command :: new ( "gh" )
197
222
. arg ( "-R" )
198
223
. arg ( repository)
@@ -201,12 +226,27 @@ fn list_issue_titles_in_milestone(
201
226
. arg ( "-m" )
202
227
. arg ( timeframe)
203
228
. arg ( "--json" )
204
- . arg ( "title" )
229
+ . arg ( "title,assignees,number " )
205
230
. output ( ) ?;
206
231
207
- let existing_issues: Vec < ExistingGithubIssue > = serde_json:: from_slice ( & output. stdout ) ?;
232
+ let existing_issues: Vec < ExistingGithubIssueJson > = serde_json:: from_slice ( & output. stdout ) ?;
208
233
209
- Ok ( existing_issues. into_iter ( ) . map ( |e_i| e_i. title ) . collect ( ) )
234
+ Ok ( existing_issues
235
+ . into_iter ( )
236
+ . map ( |e_i| {
237
+ (
238
+ e_i. title ,
239
+ ExistingGithubIssue {
240
+ number : e_i. number ,
241
+ assignees : e_i
242
+ . assignees
243
+ . iter ( )
244
+ . map ( |a| format ! ( "@{}" , a. login) )
245
+ . collect ( ) ,
246
+ } ,
247
+ )
248
+ } )
249
+ . collect ( ) )
210
250
}
211
251
/// Initializes the required `T-<team>` labels on the repository.
212
252
/// Warns if the labels are found with wrong color.
@@ -256,26 +296,50 @@ fn initialize_issues(
256
296
goal_documents : & [ GoalDocument ] ,
257
297
) -> anyhow:: Result < BTreeSet < GithubAction > > {
258
298
// the set of issues we want to exist
259
- let mut desired_issues: BTreeSet < GithubIssue > = goal_documents
299
+ let desired_issues: BTreeSet < GithubIssue > = goal_documents
260
300
. iter ( )
261
301
. map ( |goal_document| issue ( timeframe, goal_document) )
262
302
. collect :: < anyhow:: Result < _ > > ( ) ?;
263
303
264
- // remove any existings that already exist
304
+ // Compare desired issues against existing issues
265
305
let existing_issues = list_issue_titles_in_milestone ( repository, timeframe) ?;
266
- desired_issues. retain ( |i| !existing_issues. contains ( & i. title ) ) ;
306
+ let mut actions = BTreeSet :: new ( ) ;
307
+ for desired_issue in desired_issues {
308
+ match existing_issues. get ( & desired_issue. title ) {
309
+ Some ( existing_issue) => {
310
+ if existing_issue. assignees != desired_issue. assignees {
311
+ actions. insert ( GithubAction :: SyncIssue {
312
+ number : existing_issue. number ,
313
+ remove_owners : existing_issue
314
+ . assignees
315
+ . difference ( & desired_issue. assignees )
316
+ . cloned ( )
317
+ . collect ( ) ,
318
+ add_owners : desired_issue
319
+ . assignees
320
+ . difference ( & existing_issue. assignees )
321
+ . cloned ( )
322
+ . collect ( ) ,
323
+ } ) ;
324
+ }
325
+ }
267
326
268
- Ok ( desired_issues
269
- . into_iter ( )
270
- . map ( |issue| GithubAction :: CreateIssue { issue } )
271
- . collect ( ) )
327
+ None => {
328
+ actions. insert ( GithubAction :: CreateIssue {
329
+ issue : desired_issue,
330
+ } ) ;
331
+ }
332
+ }
333
+ }
334
+
335
+ Ok ( actions)
272
336
}
273
337
274
338
fn issue ( timeframe : & str , document : & GoalDocument ) -> anyhow:: Result < GithubIssue > {
275
- let mut assignees = vec ! [ ] ;
339
+ let mut assignees = BTreeSet :: default ( ) ;
276
340
for username in document. metadata . owner_usernames ( ) {
277
341
if get_person_data ( username) ?. is_some ( ) {
278
- assignees. push ( username[ 1 ..] . to_string ( ) ) ;
342
+ assignees. insert ( username[ 1 ..] . to_string ( ) ) ;
279
343
}
280
344
}
281
345
@@ -388,6 +452,9 @@ impl Display for GithubAction {
388
452
GithubAction :: CreateIssue { issue } => {
389
453
write ! ( f, "create issue \" {}\" " , issue. title)
390
454
}
455
+ GithubAction :: SyncIssue { number, .. } => {
456
+ write ! ( f, "sync issue #{}" , number)
457
+ }
391
458
}
392
459
}
393
460
}
@@ -441,7 +508,7 @@ impl GithubAction {
441
508
. arg ( "-l" )
442
509
. arg ( labels. join ( "," ) )
443
510
. arg ( "-a" )
444
- . arg ( assignees . join ( "," ) )
511
+ . arg ( comma ( & assignees ) )
445
512
. arg ( "-m" )
446
513
. arg ( & timeframe)
447
514
. output ( ) ?;
@@ -456,6 +523,43 @@ impl GithubAction {
456
523
Ok ( ( ) )
457
524
}
458
525
}
526
+ GithubAction :: SyncIssue {
527
+ number,
528
+ remove_owners,
529
+ add_owners,
530
+ } => {
531
+ let mut command = Command :: new ( "gh" ) ;
532
+ command
533
+ . arg ( "-R" )
534
+ . arg ( & repository)
535
+ . arg ( "issue" )
536
+ . arg ( "edit" )
537
+ . arg ( number. to_string ( ) ) ;
538
+
539
+ if !remove_owners. is_empty ( ) {
540
+ command. arg ( "--remove-assignee" ) . arg ( comma ( & remove_owners) ) ;
541
+ }
542
+
543
+ if !add_owners. is_empty ( ) {
544
+ command. arg ( "--add-assignee" ) . arg ( comma ( & add_owners) ) ;
545
+ }
546
+
547
+ let output = command. output ( ) ?;
548
+ if !output. status . success ( ) {
549
+ Err ( anyhow:: anyhow!(
550
+ "failed to sync issue `{}`: {}" ,
551
+ number,
552
+ String :: from_utf8_lossy( & output. stderr)
553
+ ) )
554
+ } else {
555
+ Ok ( ( ) )
556
+ }
557
+ }
459
558
}
460
559
}
461
560
}
561
+
562
+ /// Returns a comma-separated list of the strings in `s` (no spaces).
563
+ fn comma ( s : & BTreeSet < String > ) -> String {
564
+ s. iter ( ) . map ( |s| & s[ ..] ) . collect :: < Vec < _ > > ( ) . join ( "," )
565
+ }
0 commit comments