2020//! `assign.owners` config, it will auto-select an assignee based on the files
2121//! the PR modifies.
2222
23+ use crate :: db:: issue_data:: IssueData ;
2324use crate :: db:: review_prefs:: { get_review_prefs_batch, RotationMode } ;
2425use crate :: github:: UserId ;
2526use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
@@ -92,9 +93,22 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9293
9394Please choose another assignee." ;
9495
96+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewer @{username} was already assigned before.
97+
98+ Please choose another assignee by using `r? @reviewer`." ;
99+
95100// Special account that we use to prevent assignment.
96101const GHOST_ACCOUNT : & str = "ghost" ;
97102
103+ /// Key for the state in the database
104+ const PREVIOUS_REVIEWERS_KEY : & str = "previous-reviewers" ;
105+
106+ /// State stored in the database
107+ #[ derive( Debug , Clone , PartialEq , Default , serde:: Deserialize , serde:: Serialize ) ]
108+ struct Reviewers {
109+ names : HashSet < String > ,
110+ }
111+
98112/// Assignment data stored in the issue/PR body.
99113#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
100114struct AssignData {
@@ -217,7 +231,7 @@ pub(super) async fn handle_input(
217231 None
218232 } ;
219233 if let Some ( assignee) = assignee {
220- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
234+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
221235 }
222236
223237 if let Some ( welcome) = welcome {
@@ -249,15 +263,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249263}
250264
251265/// Sets the assignee of a PR, alerting any errors.
252- async fn set_assignee ( issue : & Issue , github : & GithubClient , username : & str ) {
266+ async fn set_assignee (
267+ ctx : & Context ,
268+ issue : & Issue ,
269+ github : & GithubClient ,
270+ username : & str ,
271+ ) -> anyhow:: Result < ( ) > {
272+ let mut db = ctx. db . get ( ) . await ;
273+ let mut state: IssueData < ' _ , Reviewers > =
274+ IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWERS_KEY ) . await ?;
275+
253276 // Don't re-assign if already assigned, e.g. on comment edit
254277 if issue. contain_assignee ( & username) {
255278 log:: trace!(
256279 "ignoring assign PR {} to {}, already assigned" ,
257280 issue. global_id( ) ,
258281 username,
259282 ) ;
260- return ;
283+ return Ok ( ( ) ) ;
261284 }
262285 if let Err ( err) = issue. set_assignee ( github, & username) . await {
263286 log:: warn!(
@@ -280,8 +303,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280303 . await
281304 {
282305 log:: warn!( "failed to post error comment: {e}" ) ;
306+ return Err ( e) ;
283307 }
284308 }
309+
310+ // Record the reviewer in the database
311+ state. data . names . insert ( username. to_string ( ) ) ;
312+ state. save ( ) . await ?;
313+ Ok ( ( ) )
285314}
286315
287316/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +329,12 @@ async fn determine_assignee(
300329 config : & AssignConfig ,
301330 diff : & [ FileDiff ] ,
302331) -> anyhow:: Result < ( Option < String > , bool ) > {
303- let db_client = ctx. db . get ( ) . await ;
332+ let mut db_client = ctx. db . get ( ) . await ;
304333 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305334 if let Some ( name) = assign_command {
306335 // User included `r?` in the opening PR body.
307336 match find_reviewer_from_names (
308- & db_client,
337+ & mut db_client,
309338 ctx. workqueue . clone ( ) ,
310339 & teams,
311340 config,
@@ -328,7 +357,7 @@ async fn determine_assignee(
328357 match find_reviewers_from_diff ( config, diff) {
329358 Ok ( candidates) if !candidates. is_empty ( ) => {
330359 match find_reviewer_from_names (
331- & db_client,
360+ & mut db_client,
332361 ctx. workqueue . clone ( ) ,
333362 & teams,
334363 config,
@@ -347,6 +376,7 @@ async fn determine_assignee(
347376 e @ FindReviewerError :: NoReviewer { .. }
348377 | e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349378 | e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
379+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350380 | e @ FindReviewerError :: ReviewerOffRotation { .. }
351381 | e @ FindReviewerError :: DatabaseError ( _)
352382 | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,7 +398,7 @@ async fn determine_assignee(
368398
369399 if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370400 match find_reviewer_from_names (
371- & db_client,
401+ & mut db_client,
372402 ctx. workqueue . clone ( ) ,
373403 & teams,
374404 config,
@@ -550,10 +580,9 @@ pub(super) async fn handle_command(
550580 issue. remove_assignees ( & ctx. github , Selection :: All ) . await ?;
551581 return Ok ( ( ) ) ;
552582 }
553-
554- let db_client = ctx. db . get ( ) . await ;
583+ let mut db_client = ctx. db . get ( ) . await ;
555584 let assignee = match find_reviewer_from_names (
556- & db_client,
585+ & mut db_client,
557586 ctx. workqueue . clone ( ) ,
558587 & teams,
559588 config,
@@ -569,7 +598,7 @@ pub(super) async fn handle_command(
569598 }
570599 } ;
571600
572- set_assignee ( issue, & ctx. github , & assignee) . await ;
601+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573602 } else {
574603 let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575604
@@ -680,6 +709,8 @@ enum FindReviewerError {
680709 ReviewerIsPrAuthor { username : String } ,
681710 /// Requested reviewer is already assigned to that PR
682711 ReviewerAlreadyAssigned { username : String } ,
712+ /// Requested reviewer was already assigned previously to that PR.
713+ ReviewerPreviouslyAssigned { username : String } ,
683714 /// Data required for assignment could not be loaded from the DB.
684715 DatabaseError ( String ) ,
685716 /// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +757,13 @@ impl fmt::Display for FindReviewerError {
726757 REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727758 )
728759 }
760+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
761+ write ! (
762+ f,
763+ "{}" ,
764+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
765+ )
766+ }
729767 FindReviewerError :: DatabaseError ( error) => {
730768 write ! ( f, "Database error: {error}" )
731769 }
@@ -748,7 +786,7 @@ Please select a different reviewer.",
748786/// auto-assign groups, or rust-lang team names. It must have at least one
749787/// entry.
750788async fn find_reviewer_from_names (
751- db : & DbClient ,
789+ db : & mut DbClient ,
752790 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
753791 teams : & Teams ,
754792 config : & AssignConfig ,
@@ -916,7 +954,7 @@ fn expand_teams_and_groups(
916954/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917955/// If no reviewer is available, returns an error.
918956async fn candidate_reviewers_from_names < ' a > (
919- db : & DbClient ,
957+ db : & mut DbClient ,
920958 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
921959 teams : & ' a Teams ,
922960 config : & ' a AssignConfig ,
@@ -937,6 +975,7 @@ async fn candidate_reviewers_from_names<'a>(
937975 // Set of candidate usernames to choose from.
938976 // We go through each expanded candidate and store either success or an error for them.
939977 let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
978+ let previous_reviewer_names = get_previous_reviewer_names ( db, issue) . await ;
940979
941980 // Step 2: pre-filter candidates based on checks that we can perform quickly
942981 for reviewer_candidate in expanded {
@@ -949,6 +988,8 @@ async fn candidate_reviewers_from_names<'a>(
949988 . iter ( )
950989 . any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951990
991+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
992+
952993 // Record the reason why the candidate was filtered out
953994 let reason = {
954995 if is_pr_author {
@@ -963,6 +1004,14 @@ async fn candidate_reviewers_from_names<'a>(
9631004 Some ( FindReviewerError :: ReviewerAlreadyAssigned {
9641005 username : candidate. clone ( ) ,
9651006 } )
1007+ } else if reviewer_candidate. origin == ReviewerCandidateOrigin :: Expanded
1008+ && is_previously_assigned
1009+ {
1010+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
1011+ // `r? @reviewer` will not consider the reviewer previously assigned
1012+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
1013+ username : candidate. clone ( ) ,
1014+ } )
9661015 } else {
9671016 None
9681017 }
@@ -1058,3 +1107,13 @@ async fn candidate_reviewers_from_names<'a>(
10581107 . collect ( ) )
10591108 }
10601109}
1110+
1111+ async fn get_previous_reviewer_names ( db : & mut DbClient , issue : & Issue ) -> HashSet < String > {
1112+ let state: IssueData < ' _ , Reviewers > =
1113+ match IssueData :: load ( db, & issue, PREVIOUS_REVIEWERS_KEY ) . await {
1114+ Ok ( state) => state,
1115+ Err ( _) => return HashSet :: new ( ) ,
1116+ } ;
1117+
1118+ state. data . names
1119+ }
0 commit comments