@@ -15,6 +15,8 @@ use crate::{
1515 team:: { get_person_data, TeamName } ,
1616} ;
1717
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+
1820fn validate_path ( path : & Path ) -> anyhow:: Result < String > {
1921 if !path. is_dir ( ) {
2022 return Err ( anyhow:: anyhow!(
@@ -103,50 +105,52 @@ pub fn generate_issues(
103105 commit : bool ,
104106 sleep : u64 ,
105107) -> 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) ?;
107112
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 ( ) ) ;
110115
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) ?) ;
114119
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+ }
119123
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 ,
132129 progress_bar:: Style :: Bold ,
133130 ) ;
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 ( ( ) ) ;
144152 }
145- eprintln ! ( "" ) ;
146- eprintln ! ( "Use `--commit` to execute the actions." ) ;
147153 }
148-
149- Ok ( ( ) )
150154}
151155
152156#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -167,11 +171,15 @@ enum GithubAction {
167171 } ,
168172
169173 // We intentionally do not sync the issue *text*, because it may have been edited.
170- SyncIssue {
174+ SyncAssignees {
171175 number : u64 ,
172176 remove_owners : BTreeSet < String > ,
173177 add_owners : BTreeSet < String > ,
174178 } ,
179+
180+ LockIssue {
181+ number : u64 ,
182+ } ,
175183}
176184
177185#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -183,14 +191,24 @@ struct GhLabel {
183191#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
184192struct ExistingGithubIssue {
185193 number : u64 ,
194+ /// Just github username, no `@`
186195 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 ,
187204}
188205
189206#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
190207struct ExistingGithubIssueJson {
191208 title : String ,
192209 number : u64 ,
193210 assignees : Vec < ExistingGithubAssigneeJson > ,
211+ comments : Vec < ExistingGithubCommentJson > ,
194212}
195213
196214#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , PartialOrd , Ord ) ]
@@ -199,6 +217,17 @@ struct ExistingGithubAssigneeJson {
199217 name : String ,
200218}
201219
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+
202231fn list_labels ( repository : & str ) -> anyhow:: Result < Vec < GhLabel > > {
203232 let output = Command :: new ( "gh" )
204233 . arg ( "-R" )
@@ -226,7 +255,7 @@ fn list_issue_titles_in_milestone(
226255 . arg ( "-m" )
227256 . arg ( timeframe)
228257 . arg ( "--json" )
229- . arg ( "title,assignees,number" )
258+ . arg ( "title,assignees,number,comments " )
230259 . output ( ) ?;
231260
232261 let existing_issues: Vec < ExistingGithubIssueJson > = serde_json:: from_slice ( & output. stdout ) ?;
@@ -238,10 +267,14 @@ fn list_issue_titles_in_milestone(
238267 e_i. title ,
239268 ExistingGithubIssue {
240269 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+ } )
245278 . collect ( ) ,
246279 } ,
247280 )
@@ -308,7 +341,7 @@ fn initialize_issues(
308341 match existing_issues. get ( & desired_issue. title ) {
309342 Some ( existing_issue) => {
310343 if existing_issue. assignees != desired_issue. assignees {
311- actions. insert ( GithubAction :: SyncIssue {
344+ actions. insert ( GithubAction :: SyncAssignees {
312345 number : existing_issue. number ,
313346 remove_owners : existing_issue
314347 . assignees
@@ -322,6 +355,12 @@ fn initialize_issues(
322355 . collect ( ) ,
323356 } ) ;
324357 }
358+
359+ if !existing_issue. was_locked ( ) {
360+ actions. insert ( GithubAction :: LockIssue {
361+ number : existing_issue. number ,
362+ } ) ;
363+ }
325364 }
326365
327366 None => {
@@ -335,11 +374,19 @@ fn initialize_issues(
335374 Ok ( actions)
336375}
337376
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+
338385fn issue ( timeframe : & str , document : & GoalDocument ) -> anyhow:: Result < GithubIssue > {
339386 let mut assignees = BTreeSet :: default ( ) ;
340387 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 ( ) ) ;
343390 }
344391 }
345392
@@ -452,8 +499,25 @@ impl Display for GithubAction {
452499 GithubAction :: CreateIssue { issue } => {
453500 write ! ( f, "create issue \" {}\" " , issue. title)
454501 }
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)
457521 }
458522 }
459523 }
@@ -522,8 +586,10 @@ impl GithubAction {
522586 } else {
523587 Ok ( ( ) )
524588 }
589+
590+ // Note: the issue is not locked, but we will reloop around later.
525591 }
526- GithubAction :: SyncIssue {
592+ GithubAction :: SyncAssignees {
527593 number,
528594 remove_owners,
529595 add_owners,
@@ -555,8 +621,50 @@ impl GithubAction {
555621 Ok ( ( ) )
556622 }
557623 }
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+ ) ) ;
558645 }
559646 }
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 ( ( ) )
560668}
561669
562670/// Returns a comma-separated list of the strings in `s` (no spaces).
0 commit comments