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,24 @@ 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 (
268+ ctx : & Context ,
269+ issue : & Issue ,
270+ github : & GithubClient ,
271+ username : & str ,
272+ ) -> anyhow:: Result < ( ) > {
273+ let mut db = ctx. db . get ( ) . await ;
274+ let mut state: IssueData < ' _ , Reviewers > =
275+ IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await ?;
276+
253277 // Don't re-assign if already assigned, e.g. on comment edit
254278 if issue. contain_assignee ( & username) {
255279 log:: trace!(
256280 "ignoring assign PR {} to {}, already assigned" ,
257281 issue. global_id( ) ,
258282 username,
259283 ) ;
260- return ;
284+ return Ok ( ( ) ) ;
261285 }
262286 if let Err ( err) = issue. set_assignee ( github, & username) . await {
263287 log:: warn!(
@@ -280,8 +304,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280304 . await
281305 {
282306 log:: warn!( "failed to post error comment: {e}" ) ;
307+ return Err ( e) ;
283308 }
284309 }
310+
311+ // Record the reviewer in the database
312+ state. data . names . insert ( username. to_string ( ) ) ;
313+ state. save ( ) . await ?;
314+ Ok ( ( ) )
285315}
286316
287317/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +330,12 @@ async fn determine_assignee(
300330 config : & AssignConfig ,
301331 diff : & [ FileDiff ] ,
302332) -> anyhow:: Result < ( Option < String > , bool ) > {
303- let db_client = ctx. db . get ( ) . await ;
333+ let mut db_client = ctx. db . get ( ) . await ;
304334 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305335 if let Some ( name) = assign_command {
306336 // User included `r?` in the opening PR body.
307337 match find_reviewer_from_names (
308- & db_client,
338+ & mut db_client,
309339 ctx. workqueue . clone ( ) ,
310340 & teams,
311341 config,
@@ -328,7 +358,7 @@ async fn determine_assignee(
328358 match find_reviewers_from_diff ( config, diff) {
329359 Ok ( candidates) if !candidates. is_empty ( ) => {
330360 match find_reviewer_from_names (
331- & db_client,
361+ & mut db_client,
332362 ctx. workqueue . clone ( ) ,
333363 & teams,
334364 config,
@@ -347,6 +377,7 @@ async fn determine_assignee(
347377 e @ FindReviewerError :: NoReviewer { .. }
348378 | e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349379 | e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
380+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350381 | e @ FindReviewerError :: ReviewerOffRotation { .. }
351382 | e @ FindReviewerError :: DatabaseError ( _)
352383 | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,7 +399,7 @@ async fn determine_assignee(
368399
369400 if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370401 match find_reviewer_from_names (
371- & db_client,
402+ & mut db_client,
372403 ctx. workqueue . clone ( ) ,
373404 & teams,
374405 config,
@@ -550,10 +581,9 @@ pub(super) async fn handle_command(
550581 issue. remove_assignees ( & ctx. github , Selection :: All ) . await ?;
551582 return Ok ( ( ) ) ;
552583 }
553-
554- let db_client = ctx. db . get ( ) . await ;
584+ let mut db_client = ctx. db . get ( ) . await ;
555585 let assignee = match find_reviewer_from_names (
556- & db_client,
586+ & mut db_client,
557587 ctx. workqueue . clone ( ) ,
558588 & teams,
559589 config,
@@ -569,7 +599,7 @@ pub(super) async fn handle_command(
569599 }
570600 } ;
571601
572- set_assignee ( issue, & ctx. github , & assignee) . await ;
602+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573603 } else {
574604 let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575605
@@ -680,6 +710,8 @@ enum FindReviewerError {
680710 ReviewerIsPrAuthor { username : String } ,
681711 /// Requested reviewer is already assigned to that PR
682712 ReviewerAlreadyAssigned { username : String } ,
713+ /// Requested reviewer is already assigned previously to that PR.
714+ ReviewerPreviouslyAssigned { username : String } ,
683715 /// Data required for assignment could not be loaded from the DB.
684716 DatabaseError ( String ) ,
685717 /// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +758,13 @@ impl fmt::Display for FindReviewerError {
726758 REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727759 )
728760 }
761+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
762+ write ! (
763+ f,
764+ "{}" ,
765+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
766+ )
767+ }
729768 FindReviewerError :: DatabaseError ( error) => {
730769 write ! ( f, "Database error: {error}" )
731770 }
@@ -748,7 +787,7 @@ Please select a different reviewer.",
748787/// auto-assign groups, or rust-lang team names. It must have at least one
749788/// entry.
750789async fn find_reviewer_from_names (
751- db : & DbClient ,
790+ db : & mut DbClient ,
752791 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
753792 teams : & Teams ,
754793 config : & AssignConfig ,
@@ -916,7 +955,7 @@ fn expand_teams_and_groups(
916955/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917956/// If no reviewer is available, returns an error.
918957async fn candidate_reviewers_from_names < ' a > (
919- db : & DbClient ,
958+ db : & mut DbClient ,
920959 workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
921960 teams : & ' a Teams ,
922961 config : & ' a AssignConfig ,
@@ -925,6 +964,9 @@ async fn candidate_reviewers_from_names<'a>(
925964) -> Result < HashSet < String > , FindReviewerError > {
926965 // Step 1: expand teams and groups into candidate names
927966 let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
967+ let expansion_happend = expanded
968+ . iter ( )
969+ . any ( |c| c. origin == ReviewerCandidateOrigin :: Expanded ) ;
928970 let expanded_count = expanded. len ( ) ;
929971
930972 // Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +979,7 @@ async fn candidate_reviewers_from_names<'a>(
937979 // Set of candidate usernames to choose from.
938980 // We go through each expanded candidate and store either success or an error for them.
939981 let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
982+ let previous_reviewer_names = get_previous_reviewer_names ( db, issue) . await ;
940983
941984 // Step 2: pre-filter candidates based on checks that we can perform quickly
942985 for reviewer_candidate in expanded {
@@ -949,6 +992,8 @@ async fn candidate_reviewers_from_names<'a>(
949992 . iter ( )
950993 . any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951994
995+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
996+
952997 // Record the reason why the candidate was filtered out
953998 let reason = {
954999 if is_pr_author {
@@ -963,6 +1008,12 @@ async fn candidate_reviewers_from_names<'a>(
9631008 Some ( FindReviewerError :: ReviewerAlreadyAssigned {
9641009 username : candidate. clone ( ) ,
9651010 } )
1011+ } else if expansion_happend && is_previously_assigned {
1012+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
1013+ // `r? @reviewer` will not consider the reviewer previously assigned
1014+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
1015+ username : candidate. clone ( ) ,
1016+ } )
9661017 } else {
9671018 None
9681019 }
@@ -1058,3 +1109,13 @@ async fn candidate_reviewers_from_names<'a>(
10581109 . collect ( ) )
10591110 }
10601111}
1112+
1113+ async fn get_previous_reviewer_names ( db : & mut DbClient , issue : & Issue ) -> HashSet < String > {
1114+ let state: IssueData < ' _ , Reviewers > =
1115+ match IssueData :: load ( db, & issue, PREVIOUS_REVIEWER_KEY ) . await {
1116+ Ok ( state) => state,
1117+ Err ( _) => return HashSet :: new ( ) ,
1118+ } ;
1119+
1120+ state. data . names
1121+ }
0 commit comments