@@ -15,6 +15,8 @@ use crate::{
15
15
team:: { get_person_data, TeamName } ,
16
16
} ;
17
17
18
+ const LOCK_TEXT : & str = "This issue is intended for status updates only.\n \n For general questions or comments, please contact the owner(s) directly." ;
19
+
18
20
fn validate_path ( path : & Path ) -> anyhow:: Result < String > {
19
21
if !path. is_dir ( ) {
20
22
return Err ( anyhow:: anyhow!(
@@ -103,50 +105,52 @@ pub fn generate_issues(
103
105
commit : bool ,
104
106
sleep : u64 ,
105
107
) -> anyhow:: Result < ( ) > {
106
- let timeframe = validate_path ( path) ?;
108
+ // Hacky but works: we loop because after creating the issue, we sometimes have additional sync to do,
109
+ // and it's easier this way.
110
+ loop {
111
+ let timeframe = validate_path ( path) ?;
107
112
108
- let mut goal_documents = goal:: goals_in_dir ( path) ?;
109
- goal_documents. retain ( |gd| gd. is_not_not_accepted ( ) ) ;
113
+ let mut goal_documents = goal:: goals_in_dir ( path) ?;
114
+ goal_documents. retain ( |gd| gd. is_not_not_accepted ( ) ) ;
110
115
111
- let teams_with_asks = teams_with_asks ( & goal_documents) ;
112
- let mut actions = initialize_labels ( repository, & teams_with_asks) ?;
113
- actions. extend ( initialize_issues ( repository, & timeframe, & goal_documents) ?) ;
116
+ let teams_with_asks = teams_with_asks ( & goal_documents) ;
117
+ let mut actions = initialize_labels ( repository, & teams_with_asks) ?;
118
+ actions. extend ( initialize_issues ( repository, & timeframe, & goal_documents) ?) ;
114
119
115
- if actions. is_empty ( ) {
116
- eprintln ! ( "No actions to be executed." ) ;
117
- return Ok ( ( ) ) ;
118
- }
120
+ if actions. is_empty ( ) {
121
+ return Ok ( ( ) ) ;
122
+ }
119
123
120
- if commit {
121
- progress_bar:: init_progress_bar ( actions. len ( ) ) ;
122
- progress_bar:: set_progress_bar_action (
123
- "Executing" ,
124
- progress_bar:: Color :: Blue ,
125
- progress_bar:: Style :: Bold ,
126
- ) ;
127
- for action in actions. into_iter ( ) {
128
- progress_bar:: print_progress_bar_info (
129
- "Action" ,
130
- & format ! ( "{}" , action) ,
131
- progress_bar:: Color :: Green ,
124
+ if commit {
125
+ progress_bar:: init_progress_bar ( actions. len ( ) ) ;
126
+ progress_bar:: set_progress_bar_action (
127
+ "Executing" ,
128
+ progress_bar:: Color :: Blue ,
132
129
progress_bar:: Style :: Bold ,
133
130
) ;
134
- action. execute ( repository, & timeframe) ?;
135
- progress_bar:: inc_progress_bar ( ) ;
136
-
137
- std:: thread:: sleep ( Duration :: from_millis ( sleep) ) ;
138
- }
139
- progress_bar:: finalize_progress_bar ( ) ;
140
- } else {
141
- eprintln ! ( "Actions to be executed:" ) ;
142
- for action in & actions {
143
- eprintln ! ( "* {action}" ) ;
131
+ for action in actions. into_iter ( ) {
132
+ progress_bar:: print_progress_bar_info (
133
+ "Action" ,
134
+ & format ! ( "{}" , action) ,
135
+ progress_bar:: Color :: Green ,
136
+ progress_bar:: Style :: Bold ,
137
+ ) ;
138
+ action. execute ( repository, & timeframe) ?;
139
+ progress_bar:: inc_progress_bar ( ) ;
140
+
141
+ std:: thread:: sleep ( Duration :: from_millis ( sleep) ) ;
142
+ }
143
+ progress_bar:: finalize_progress_bar ( ) ;
144
+ } else {
145
+ eprintln ! ( "Actions to be executed:" ) ;
146
+ for action in & actions {
147
+ eprintln ! ( "* {action}" ) ;
148
+ }
149
+ eprintln ! ( "" ) ;
150
+ eprintln ! ( "Use `--commit` to execute the actions." ) ;
151
+ return Ok ( ( ) ) ;
144
152
}
145
- eprintln ! ( "" ) ;
146
- eprintln ! ( "Use `--commit` to execute the actions." ) ;
147
153
}
148
-
149
- Ok ( ( ) )
150
154
}
151
155
152
156
#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -167,11 +171,15 @@ enum GithubAction {
167
171
} ,
168
172
169
173
// We intentionally do not sync the issue *text*, because it may have been edited.
170
- SyncIssue {
174
+ SyncAssignees {
171
175
number : u64 ,
172
176
remove_owners : BTreeSet < String > ,
173
177
add_owners : BTreeSet < String > ,
174
178
} ,
179
+
180
+ LockIssue {
181
+ number : u64 ,
182
+ } ,
175
183
}
176
184
177
185
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -183,14 +191,24 @@ struct GhLabel {
183
191
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
184
192
struct ExistingGithubIssue {
185
193
number : u64 ,
194
+ /// Just github username, no `@`
186
195
assignees : BTreeSet < String > ,
196
+ comments : Vec < ExistingGithubComment > ,
197
+ }
198
+
199
+ #[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
200
+ struct ExistingGithubComment {
201
+ /// Just github username, no `@`
202
+ author : String ,
203
+ body : String ,
187
204
}
188
205
189
206
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
190
207
struct ExistingGithubIssueJson {
191
208
title : String ,
192
209
number : u64 ,
193
210
assignees : Vec < ExistingGithubAssigneeJson > ,
211
+ comments : Vec < ExistingGithubCommentJson > ,
194
212
}
195
213
196
214
#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -199,6 +217,17 @@ struct ExistingGithubAssigneeJson {
199
217
name : String ,
200
218
}
201
219
220
+ #[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
221
+ struct ExistingGithubCommentJson {
222
+ body : String ,
223
+ author : ExistingGithubAuthorJson ,
224
+ }
225
+
226
+ #[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
227
+ struct ExistingGithubAuthorJson {
228
+ login : String ,
229
+ }
230
+
202
231
fn list_labels ( repository : & str ) -> anyhow:: Result < Vec < GhLabel > > {
203
232
let output = Command :: new ( "gh" )
204
233
. arg ( "-R" )
@@ -226,7 +255,7 @@ fn list_issue_titles_in_milestone(
226
255
. arg ( "-m" )
227
256
. arg ( timeframe)
228
257
. arg ( "--json" )
229
- . arg ( "title,assignees,number" )
258
+ . arg ( "title,assignees,number,comments " )
230
259
. output ( ) ?;
231
260
232
261
let existing_issues: Vec < ExistingGithubIssueJson > = serde_json:: from_slice ( & output. stdout ) ?;
@@ -238,10 +267,14 @@ fn list_issue_titles_in_milestone(
238
267
e_i. title ,
239
268
ExistingGithubIssue {
240
269
number : e_i. number ,
241
- assignees : e_i
242
- . assignees
243
- . iter ( )
244
- . map ( |a| format ! ( "@{}" , a. login) )
270
+ assignees : e_i. assignees . into_iter ( ) . map ( |a| a. login ) . collect ( ) ,
271
+ comments : e_i
272
+ . comments
273
+ . into_iter ( )
274
+ . map ( |c| ExistingGithubComment {
275
+ author : format ! ( "@{}" , c. author. login) ,
276
+ body : c. body ,
277
+ } )
245
278
. collect ( ) ,
246
279
} ,
247
280
)
@@ -308,7 +341,7 @@ fn initialize_issues(
308
341
match existing_issues. get ( & desired_issue. title ) {
309
342
Some ( existing_issue) => {
310
343
if existing_issue. assignees != desired_issue. assignees {
311
- actions. insert ( GithubAction :: SyncIssue {
344
+ actions. insert ( GithubAction :: SyncAssignees {
312
345
number : existing_issue. number ,
313
346
remove_owners : existing_issue
314
347
. assignees
@@ -322,6 +355,12 @@ fn initialize_issues(
322
355
. collect ( ) ,
323
356
} ) ;
324
357
}
358
+
359
+ if !existing_issue. was_locked ( ) {
360
+ actions. insert ( GithubAction :: LockIssue {
361
+ number : existing_issue. number ,
362
+ } ) ;
363
+ }
325
364
}
326
365
327
366
None => {
@@ -335,11 +374,19 @@ fn initialize_issues(
335
374
Ok ( actions)
336
375
}
337
376
377
+ impl ExistingGithubIssue {
378
+ /// We use the presence of a "lock comment" as a signal that we successfully locked the issue.
379
+ /// The github CLI doesn't let you query that directly.
380
+ fn was_locked ( & self ) -> bool {
381
+ self . comments . iter ( ) . any ( |c| c. body . trim ( ) == LOCK_TEXT )
382
+ }
383
+ }
384
+
338
385
fn issue ( timeframe : & str , document : & GoalDocument ) -> anyhow:: Result < GithubIssue > {
339
386
let mut assignees = BTreeSet :: default ( ) ;
340
387
for username in document. metadata . owner_usernames ( ) {
341
- if get_person_data ( username) ?. is_some ( ) {
342
- assignees. insert ( username [ 1 .. ] . to_string ( ) ) ;
388
+ if let Some ( data ) = get_person_data ( username) ? {
389
+ assignees. insert ( data . github_username . clone ( ) ) ;
343
390
}
344
391
}
345
392
@@ -452,8 +499,25 @@ impl Display for GithubAction {
452
499
GithubAction :: CreateIssue { issue } => {
453
500
write ! ( f, "create issue \" {}\" " , issue. title)
454
501
}
455
- GithubAction :: SyncIssue { number, .. } => {
456
- write ! ( f, "sync issue #{}" , number)
502
+ GithubAction :: SyncAssignees {
503
+ number,
504
+ remove_owners,
505
+ add_owners,
506
+ } => {
507
+ write ! (
508
+ f,
509
+ "sync issue #{} ({})" ,
510
+ number,
511
+ remove_owners
512
+ . iter( )
513
+ . map( |s| format!( "-{}" , s) )
514
+ . chain( add_owners. iter( ) . map( |s| format!( "+{}" , s) ) )
515
+ . collect:: <Vec <_>>( )
516
+ . join( ", " )
517
+ )
518
+ }
519
+ GithubAction :: LockIssue { number } => {
520
+ write ! ( f, "lock issue #{}" , number)
457
521
}
458
522
}
459
523
}
@@ -522,8 +586,10 @@ impl GithubAction {
522
586
} else {
523
587
Ok ( ( ) )
524
588
}
589
+
590
+ // Note: the issue is not locked, but we will reloop around later.
525
591
}
526
- GithubAction :: SyncIssue {
592
+ GithubAction :: SyncAssignees {
527
593
number,
528
594
remove_owners,
529
595
add_owners,
@@ -555,8 +621,50 @@ impl GithubAction {
555
621
Ok ( ( ) )
556
622
}
557
623
}
624
+ GithubAction :: LockIssue { number } => lock_issue ( repository, number) ,
625
+ }
626
+ }
627
+ }
628
+
629
+ fn lock_issue ( repository : & str , number : u64 ) -> anyhow:: Result < ( ) > {
630
+ let output = Command :: new ( "gh" )
631
+ . arg ( "-R" )
632
+ . arg ( repository)
633
+ . arg ( "issue" )
634
+ . arg ( "lock" )
635
+ . arg ( number. to_string ( ) )
636
+ . output ( ) ?;
637
+
638
+ if !output. status . success ( ) {
639
+ if !output. stderr . starts_with ( b"already locked" ) {
640
+ return Err ( anyhow:: anyhow!(
641
+ "failed to lock issue `{}`: {}" ,
642
+ number,
643
+ String :: from_utf8_lossy( & output. stderr)
644
+ ) ) ;
558
645
}
559
646
}
647
+
648
+ // Leave a comment explaining what is going on.
649
+ let output = Command :: new ( "gh" )
650
+ . arg ( "-R" )
651
+ . arg ( repository)
652
+ . arg ( "issue" )
653
+ . arg ( "comment" )
654
+ . arg ( number. to_string ( ) )
655
+ . arg ( "-b" )
656
+ . arg ( LOCK_TEXT )
657
+ . output ( ) ?;
658
+
659
+ if !output. status . success ( ) {
660
+ return Err ( anyhow:: anyhow!(
661
+ "failed to leave lock comment `{}`: {}" ,
662
+ number,
663
+ String :: from_utf8_lossy( & output. stderr)
664
+ ) ) ;
665
+ }
666
+
667
+ Ok ( ( ) )
560
668
}
561
669
562
670
/// Returns a comma-separated list of the strings in `s` (no spaces).
0 commit comments