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,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9293
9394Please choose another assignee." ;
9495
96+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewers are 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_REVIEWER_KEY : & str = "previous-reviewer" ;
105+
106+ /// State stored in the database
107+ #[ derive( Debug , Clone , PartialEq , Default , serde:: Deserialize , serde:: Serialize ) ]
108+ struct Reviewers {
109+ /// List of the last warnings in the most recent comment.
110+ names : HashSet < String > ,
111+ }
112+
98113/// Assignment data stored in the issue/PR body.
99114#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
100115struct AssignData {
@@ -217,7 +232,7 @@ pub(super) async fn handle_input(
217232 None
218233 } ;
219234 if let Some ( assignee) = assignee {
220- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
235+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
221236 }
222237
223238 if let Some ( welcome) = welcome {
@@ -249,15 +264,19 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249264}
250265
251266/// Sets the assignee of a PR, alerting any errors.
252- async fn set_assignee ( issue : & Issue , github : & GithubClient , username : & str ) {
267+ async fn set_assignee ( ctx : & Context , issue : & Issue , github : & GithubClient , username : & str ) -> anyhow:: Result < ( ) > {
268+ let mut db = ctx. db . get ( ) . await ;
269+ let mut state: IssueData < ' _ , Reviewers > =
270+ IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await ?;
271+
253272 // Don't re-assign if already assigned, e.g. on comment edit
254273 if issue. contain_assignee ( & username) {
255274 log:: trace!(
256275 "ignoring assign PR {} to {}, already assigned" ,
257276 issue. global_id( ) ,
258277 username,
259278 ) ;
260- return ;
279+ return Ok ( ( ) ) ;
261280 }
262281 if let Err ( err) = issue. set_assignee ( github, & username) . await {
263282 log:: warn!(
@@ -280,8 +299,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280299 . await
281300 {
282301 log:: warn!( "failed to post error comment: {e}" ) ;
302+ return Err ( e) ;
283303 }
284304 }
305+
306+ // Record the reviewer in the database
307+ state. data . names . insert ( username. to_string ( ) ) ;
308+ state. save ( ) . await ?;
309+ Ok ( ( ) )
285310}
286311
287312/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +325,12 @@ async fn determine_assignee(
300325 config : & AssignConfig ,
301326 diff : & [ FileDiff ] ,
302327) -> anyhow:: Result < ( Option < String > , bool ) > {
303- let db_client = ctx. db . get ( ) . await ;
328+ let mut db_client = ctx. db . get ( ) . await ;
304329 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305330 if let Some ( name) = assign_command {
306331 // User included `r?` in the opening PR body.
307332 match find_reviewer_from_names (
308- & db_client,
333+ & mut db_client,
309334 ctx. workqueue . clone ( ) ,
310335 & teams,
311336 config,
@@ -328,7 +353,7 @@ async fn determine_assignee(
328353 match find_reviewers_from_diff ( config, diff) {
329354 Ok ( candidates) if !candidates. is_empty ( ) => {
330355 match find_reviewer_from_names (
331- & db_client,
356+ & mut db_client,
332357 ctx. workqueue . clone ( ) ,
333358 & teams,
334359 config,
@@ -347,6 +372,7 @@ async fn determine_assignee(
347372 e @ FindReviewerError :: NoReviewer { .. }
348373 | e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349374 | e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
375+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350376 | e @ FindReviewerError :: ReviewerOffRotation { .. }
351377 | e @ FindReviewerError :: DatabaseError ( _)
352378 | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,7 +394,7 @@ async fn determine_assignee(
368394
369395 if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370396 match find_reviewer_from_names (
371- & db_client,
397+ & mut db_client,
372398 ctx. workqueue . clone ( ) ,
373399 & teams,
374400 config,
@@ -550,10 +576,9 @@ pub(super) async fn handle_command(
550576 issue. remove_assignees ( & ctx. github , Selection :: All ) . await ?;
551577 return Ok ( ( ) ) ;
552578 }
553-
554- let db_client = ctx. db . get ( ) . await ;
579+ let mut db_client = ctx. db . get ( ) . await ;
555580 let assignee = match find_reviewer_from_names (
556- & db_client,
581+ & mut db_client,
557582 ctx. workqueue . clone ( ) ,
558583 & teams,
559584 config,
@@ -569,7 +594,7 @@ pub(super) async fn handle_command(
569594 }
570595 } ;
571596
572- set_assignee ( issue, & ctx. github , & assignee) . await ;
597+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573598 } else {
574599 let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575600
@@ -680,6 +705,8 @@ enum FindReviewerError {
680705 ReviewerIsPrAuthor { username : String } ,
681706 /// Requested reviewer is already assigned to that PR
682707 ReviewerAlreadyAssigned { username : String } ,
708+ /// Requested reviewer is already assigned previously to that PR.
709+ ReviewerPreviouslyAssigned { username : String } ,
683710 /// Data required for assignment could not be loaded from the DB.
684711 DatabaseError ( String ) ,
685712 /// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +753,13 @@ impl fmt::Display for FindReviewerError {
726753 REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727754 )
728755 }
756+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
757+ write ! (
758+ f,
759+ "{}" ,
760+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
761+ )
762+ }
729763 FindReviewerError :: DatabaseError ( error) => {
730764 write ! ( f, "Database error: {error}" )
731765 }
@@ -748,7 +782,7 @@ Please select a different reviewer.",
748782/// auto-assign groups, or rust-lang team names. It must have at least one
749783/// entry.
750784async fn find_reviewer_from_names (
751- db : & DbClient ,
785+ db : & mut DbClient ,
752786 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
753787 teams : & Teams ,
754788 config : & AssignConfig ,
@@ -916,7 +950,7 @@ fn expand_teams_and_groups(
916950/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917951/// If no reviewer is available, returns an error.
918952async fn candidate_reviewers_from_names < ' a > (
919- db : & DbClient ,
953+ db : & mut DbClient ,
920954 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
921955 teams : & ' a Teams ,
922956 config : & ' a AssignConfig ,
@@ -925,6 +959,7 @@ async fn candidate_reviewers_from_names<'a>(
925959) -> Result < HashSet < String > , FindReviewerError > {
926960 // Step 1: expand teams and groups into candidate names
927961 let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
962+ let expansion_happend = expanded. iter ( ) . any ( |c| c. origin == ReviewerCandidateOrigin :: Expanded ) ;
928963 let expanded_count = expanded. len ( ) ;
929964
930965 // Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +972,7 @@ async fn candidate_reviewers_from_names<'a>(
937972 // Set of candidate usernames to choose from.
938973 // We go through each expanded candidate and store either success or an error for them.
939974 let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
975+ let previous_reviewer_names = get_previous_reviewer_names ( db, issue) . await ;
940976
941977 // Step 2: pre-filter candidates based on checks that we can perform quickly
942978 for reviewer_candidate in expanded {
@@ -949,6 +985,8 @@ async fn candidate_reviewers_from_names<'a>(
949985 . iter ( )
950986 . any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951987
988+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
989+
952990 // Record the reason why the candidate was filtered out
953991 let reason = {
954992 if is_pr_author {
@@ -963,6 +1001,12 @@ async fn candidate_reviewers_from_names<'a>(
9631001 Some ( FindReviewerError :: ReviewerAlreadyAssigned {
9641002 username : candidate. clone ( ) ,
9651003 } )
1004+ } else if expansion_happend && is_previously_assigned {
1005+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
1006+ // `r? @reviewer` will not consider the reviewer previously assigned
1007+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
1008+ username : candidate. clone ( ) ,
1009+ } )
9661010 } else {
9671011 None
9681012 }
@@ -1058,3 +1102,13 @@ async fn candidate_reviewers_from_names<'a>(
10581102 . collect ( ) )
10591103 }
10601104}
1105+
1106+ async fn get_previous_reviewer_names ( db : & mut DbClient , issue : & Issue ) -> HashSet < String > {
1107+ let state: IssueData < ' _ , Reviewers > =
1108+ match IssueData :: load ( db, & issue, PREVIOUS_REVIEWER_KEY ) . await {
1109+ Ok ( state) => state,
1110+ Err ( _) => return HashSet :: new ( ) ,
1111+ } ;
1112+
1113+ state. data . names
1114+ }
0 commit comments