@@ -13,7 +13,7 @@ use crate::handlers::docs_update::docs_update;
1313use  crate :: handlers:: pr_tracking:: get_assigned_prs; 
1414use  crate :: handlers:: project_goals:: { self ,  ping_project_goals_owners} ; 
1515use  crate :: interactions:: ErrorComment ; 
16- use  crate :: utils:: pluralize; 
16+ use  crate :: utils:: { contains_any ,   pluralize} ; 
1717use  crate :: zulip:: api:: { MessageApiResponse ,  Recipient } ; 
1818use  crate :: zulip:: client:: ZulipClient ; 
1919use  crate :: zulip:: commands:: { 
@@ -24,12 +24,34 @@ use axum::Json;
2424use  axum:: extract:: State ; 
2525use  axum:: extract:: rejection:: JsonRejection ; 
2626use  axum:: response:: IntoResponse ; 
27+ use  commands:: BackportArgs ; 
28+ use  octocrab:: Octocrab ; 
2729use  rust_team_data:: v1:: { TeamKind ,  TeamMember } ; 
2830use  std:: cmp:: Reverse ; 
2931use  std:: fmt:: Write  as  _; 
3032use  std:: sync:: Arc ; 
3133use  subtle:: ConstantTimeEq ; 
32- use  tracing as  log; 
34+ use  tracing:: log; 
35+ 
36+ fn  get_text_backport_approved ( channel :  & str ,  verb :  & str ,  zulip_link :  & str )  -> String  { 
37+     format ! ( " 
38+ {channel} backport {verb} as per compiler team [on Zulip]({zulip_link}). A backport PR will be authored by the release team at the end of the current development cycle. Backport labels handled by them. 
39+ 
40+ @rustbot label +{channel}-accepted" ) 
41+ } 
42+ 
43+ fn  get_text_backport_declined ( channel :  & str ,  verb :  & str ,  zulip_link :  & str )  -> String  { 
44+     format ! ( 
45+         " 
46+ {channel} backport {verb} as per compiler team [on Zulip]({zulip_link}). 
47+ 
48+ @rustbot label -{channel}-nominated" 
49+     ) 
50+ } 
51+ 
52+ const  BACKPORT_CHANNELS :  [ & str ;  2 ]  = [ "beta" ,  "stable" ] ; 
53+ const  BACKPORT_VERBS_APPROVE :  [ & str ;  4 ]  = [ "accept" ,  "accepted" ,  "approve" ,  "approved" ] ; 
54+ const  BACKPORT_VERBS_DECLINE :  [ & str ;  2 ]  = [ "decline" ,  "declined" ] ; 
3355
3456#[ derive( Debug ,  serde:: Deserialize ) ]  
3557pub  struct  Request  { 
@@ -302,10 +324,71 @@ async fn handle_command<'a>(
302324                . map_err ( |e| format_err ! ( "Failed to await at this time: {e:?}" ) ) , 
303325            StreamCommand :: PingGoals ( args)  => ping_goals_cmd ( ctx,  gh_id,  message_data,  & args) . await , 
304326            StreamCommand :: DocsUpdate  => trigger_docs_update ( message_data,  & ctx. zulip ) , 
327+             StreamCommand :: Backport ( args)  => { 
328+                 accept_decline_backport ( message_data,  & ctx. octocrab ,  & ctx. zulip ,  & args) . await 
329+             } 
305330        } 
306331    } 
307332} 
308333
334+ // TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload 
335+ async  fn  accept_decline_backport ( 
336+     message_data :  & Message , 
337+     octo_client :  & Octocrab , 
338+     zulip_client :  & ZulipClient , 
339+     args_data :  & BackportArgs , 
340+ )  -> anyhow:: Result < Option < String > >  { 
341+     let  message = message_data. clone ( ) ; 
342+     let  args = args_data. clone ( ) ; 
343+     let  stream_id = message. stream_id . unwrap ( ) ; 
344+     let  subject = message. subject . unwrap ( ) ; 
345+     let  verb = args. verb . to_lowercase ( ) ; 
346+     let  octo_client = octo_client. clone ( ) ; 
347+ 
348+     // Repository owner and name are hardcoded 
349+     // This command is only used in this repository 
350+     let  repo_owner = "rust-lang" ; 
351+     let  repo_name = "rust" ; 
352+ 
353+     // validate command parameters 
354+     if  !contains_any ( & [ args. channel . to_lowercase ( ) . as_str ( ) ] ,  & BACKPORT_CHANNELS )  { 
355+         return  Err ( anyhow:: anyhow!( 
356+             "Parser error: unknown channel (allowed: {BACKPORT_CHANNELS:?})." 
357+         ) ) ; 
358+     } 
359+ 
360+     // TODO: factor out the Zulip "URL encoder" to make it practical to use 
361+     let  zulip_send_req = crate :: zulip:: MessageApiRequest  { 
362+         recipient :  Recipient :: Stream  { 
363+             id :  stream_id, 
364+             topic :  & subject, 
365+         } , 
366+         content :  "" , 
367+     } ; 
368+     let  zulip_link = zulip_send_req. url ( zulip_client) ; 
369+ 
370+     let  message_body = if  contains_any ( & [ verb. as_str ( ) ] ,  & BACKPORT_VERBS_APPROVE )  { 
371+         get_text_backport_approved ( & args. channel ,  & verb,  & zulip_link) 
372+     }  else  if  contains_any ( & [ verb. as_str ( ) ] ,  & BACKPORT_VERBS_DECLINE )  { 
373+         get_text_backport_declined ( & args. channel ,  & verb,  & zulip_link) 
374+     }  else  { 
375+         return  Err ( anyhow:: anyhow!( 
376+             "Parser error: unknown verb (allowed: {BACKPORT_VERBS_APPROVE:?} or {BACKPORT_VERBS_DECLINE:?})" 
377+         ) ) ; 
378+     } ; 
379+ 
380+     let  res = octo_client
381+         . issues ( repo_owner,  repo_name) 
382+         . create_comment ( args. pr_num ,  & message_body) 
383+         . await 
384+         . context ( "unable to post comment on #{args.pr_num}" ) ; 
385+     if  res. is_err ( )  { 
386+         tracing:: error!( "failed to post comment: {0:?}" ,  res. err( ) ) ; 
387+     } 
388+ 
389+     Ok ( Some ( "" . to_string ( ) ) ) 
390+ } 
391+ 
309392async  fn  ping_goals_cmd ( 
310393    ctx :  Arc < Context > , 
311394    gh_id :  u64 , 
0 commit comments