55//! * `@rustbot assign @gh-user`: Assigns to the given user.
66//! * `@rustbot claim`: Assigns to the comment author.
77//! * `@rustbot release-assignment`: Removes the commenter's assignment.
8+ //! * `@rustbot reroll`: Assigns a new reviewer, excluding the current assignee.
89//! * `r? @user`: Assigns to the given user (PRs only).
910//!
1011//! Note: this module does not handle review assignments issued from the
@@ -134,7 +135,7 @@ pub(super) async fn handle_input(
134135
135136 // Don't auto-assign or welcome if the user manually set the assignee when opening.
136137 if event. issue . assignees . is_empty ( ) {
137- let ( assignee, from_comment) = determine_assignee ( ctx, event, config, & diff) . await ?;
138+ let ( assignee, from_comment) = determine_assignee ( ctx, event, config, & diff, & [ ] ) . await ?;
138139 if assignee. as_deref ( ) == Some ( GHOST_ACCOUNT ) {
139140 // "ghost" is GitHub's placeholder account for deleted accounts.
140141 // It is used here as a convenient way to prevent assignment. This
@@ -202,6 +203,7 @@ fn find_assign_command(ctx: &Context, event: &IssuesEvent) -> Option<String> {
202203 let mut input = Input :: new ( & event. issue . body , vec ! [ & ctx. username] ) ;
203204 input. find_map ( |command| match command {
204205 Command :: Assign ( Ok ( AssignCommand :: RequestReview { name } ) ) => Some ( name) ,
206+ Command :: Assign ( Ok ( AssignCommand :: Reroll ) ) => None ,
205207 _ => None ,
206208 } )
207209}
@@ -260,6 +262,7 @@ async fn determine_assignee(
260262 event : & IssuesEvent ,
261263 config : & AssignConfig ,
262264 diff : & [ FileDiff ] ,
265+ exclude_assignees : & [ String ] ,
263266) -> anyhow:: Result < ( Option < String > , bool ) > {
264267 let db_client = ctx. db . get ( ) . await ;
265268 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
@@ -279,8 +282,18 @@ async fn determine_assignee(
279282 // Errors fall-through to try fallback group.
280283 match find_reviewers_from_diff ( config, diff) {
281284 Ok ( candidates) if !candidates. is_empty ( ) => {
282- match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , & candidates)
283- . await
285+ let filtered_candidates = candidates
286+ . into_iter ( )
287+ . filter ( |candidate| !exclude_assignees. contains ( candidate) )
288+ . collect :: < Vec < _ > > ( ) ;
289+ match find_reviewer_from_names (
290+ & db_client,
291+ & teams,
292+ config,
293+ & event. issue ,
294+ & filtered_candidates,
295+ )
296+ . await
284297 {
285298 Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
286299 Err ( FindReviewerError :: TeamNotFound ( team) ) => log:: warn!(
@@ -450,6 +463,44 @@ pub(super) async fn handle_command(
450463 let assignee = match cmd {
451464 AssignCommand :: Claim => event. user ( ) . login . clone ( ) ,
452465 AssignCommand :: AssignUser { username } => username,
466+ AssignCommand :: Reroll => {
467+ // Get the current assignees and make sure we don't select them again
468+ let current_assignees: Vec < String > =
469+ issue. assignees . iter ( ) . map ( |a| a. login . clone ( ) ) . collect ( ) ;
470+
471+ if current_assignees. is_empty ( ) {
472+ issue
473+ . post_comment (
474+ & ctx. github ,
475+ "Cannot reroll because there is no current assignee." ,
476+ )
477+ . await ?;
478+ return Ok ( ( ) ) ;
479+ }
480+
481+ // Get PR diff to find candidates
482+ let Some ( diff) = issue. diff ( & ctx. github ) . await ? else {
483+ log:: error!( "Failed to get PR diff for reroll." ) ;
484+ return Ok ( ( ) ) ;
485+ } ;
486+
487+ let Event :: Issue ( issue_event) = event else {
488+ log:: error!( "Failed to get IssuesEvent for reroll." ) ;
489+ return Ok ( ( ) ) ;
490+ } ;
491+
492+ let ( Some ( new_assignee) , _) =
493+ determine_assignee ( ctx, & issue_event, config, diff, & current_assignees) . await ?
494+ else {
495+ issue. post_comment (
496+ & ctx. github ,
497+ "Failed to determine new assignee for reroll. Please try r? @author to pick a new reviewer." ,
498+ ) . await ?;
499+ return Ok ( ( ) ) ;
500+ } ;
501+
502+ new_assignee
503+ }
453504 AssignCommand :: ReleaseAssignment => {
454505 log:: trace!(
455506 "ignoring release on PR {:?}, must always have assignee" ,
@@ -542,6 +593,7 @@ pub(super) async fn handle_command(
542593 } ;
543594 }
544595 AssignCommand :: RequestReview { .. } => bail ! ( "r? is only allowed on PRs." ) ,
596+ AssignCommand :: Reroll => bail ! ( "Reroll is only allowed on PRs." ) ,
545597 } ;
546598 // Don't re-assign if aleady assigned, e.g. on comment edit
547599 if issue. contain_assignee ( & to_assign) {
0 commit comments