2020//! `assign.owners` config, it will auto-select an assignee based on the files
2121//! the PR modifies.
2222
23- use crate :: db:: review_prefs:: { get_review_prefs_batch, RotationMode } ;
23+ use crate :: db:: issue_data:: IssueData ;
24+ use crate :: db:: review_prefs:: get_review_prefs_batch;
2425use crate :: github:: UserId ;
25- use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
2626use crate :: {
2727 config:: AssignConfig ,
2828 github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
@@ -36,9 +36,6 @@ use rand::seq::IteratorRandom;
3636use rust_team_data:: v1:: Teams ;
3737use std:: collections:: { HashMap , HashSet } ;
3838use std:: fmt;
39- use std:: sync:: Arc ;
40- use tokio:: sync:: RwLock ;
41- use tokio_postgres:: Client as DbClient ;
4239use tracing as log;
4340
4441#[ cfg( test) ]
@@ -92,9 +89,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9289
9390Please choose another assignee." ;
9491
92+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewers are assigned before.
93+
94+ Please choose another assignee by using `r? @reviewer`." ;
95+
9596// Special account that we use to prevent assignment.
9697const GHOST_ACCOUNT : & str = "ghost" ;
9798
99+ /// Key for the state in the database
100+ const PREVIOUS_REVIEWER_KEY : & str = "previous-reviewer" ;
101+
102+ /// State stored in the database
103+ #[ derive( Debug , Clone , PartialEq , Default , serde:: Deserialize , serde:: Serialize ) ]
104+ struct Reviewers {
105+ /// List of the last warnings in the most recent comment.
106+ names : HashSet < String > ,
107+ }
108+
98109/// Assignment data stored in the issue/PR body.
99110#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
100111struct AssignData {
@@ -217,7 +228,7 @@ pub(super) async fn handle_input(
217228 None
218229 } ;
219230 if let Some ( assignee) = assignee {
220- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
231+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
221232 }
222233
223234 if let Some ( welcome) = welcome {
@@ -249,15 +260,19 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249260}
250261
251262/// Sets the assignee of a PR, alerting any errors.
252- async fn set_assignee ( issue : & Issue , github : & GithubClient , username : & str ) {
263+ async fn set_assignee ( ctx : & Context , issue : & Issue , github : & GithubClient , username : & str ) -> anyhow:: Result < ( ) > {
264+ let mut db = ctx. db . get ( ) . await ;
265+ let mut state: IssueData < ' _ , Reviewers > =
266+ IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await ?;
267+
253268 // Don't re-assign if already assigned, e.g. on comment edit
254269 if issue. contain_assignee ( & username) {
255270 log:: trace!(
256271 "ignoring assign PR {} to {}, already assigned" ,
257272 issue. global_id( ) ,
258273 username,
259274 ) ;
260- return ;
275+ return Ok ( ( ) ) ;
261276 }
262277 if let Err ( err) = issue. set_assignee ( github, & username) . await {
263278 log:: warn!(
@@ -280,8 +295,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280295 . await
281296 {
282297 log:: warn!( "failed to post error comment: {e}" ) ;
298+ return Err ( e) ;
283299 }
284300 }
301+
302+ // Record the reviewer in the database
303+ state. data . names . insert ( username. to_string ( ) ) ;
304+ state. save ( ) . await ?;
305+ Ok ( ( ) )
285306}
286307
287308/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,13 +321,11 @@ async fn determine_assignee(
300321 config : & AssignConfig ,
301322 diff : & [ FileDiff ] ,
302323) -> anyhow:: Result < ( Option < String > , bool ) > {
303- let db_client = ctx. db . get ( ) . await ;
304324 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305325 if let Some ( name) = assign_command {
306326 // User included `r?` in the opening PR body.
307327 match find_reviewer_from_names (
308- & db_client,
309- ctx. workqueue . clone ( ) ,
328+ ctx,
310329 & teams,
311330 config,
312331 & event. issue ,
@@ -328,8 +347,7 @@ async fn determine_assignee(
328347 match find_reviewers_from_diff ( config, diff) {
329348 Ok ( candidates) if !candidates. is_empty ( ) => {
330349 match find_reviewer_from_names (
331- & db_client,
332- ctx. workqueue . clone ( ) ,
350+ ctx,
333351 & teams,
334352 config,
335353 & event. issue ,
@@ -347,6 +365,7 @@ async fn determine_assignee(
347365 e @ FindReviewerError :: NoReviewer { .. }
348366 | e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349367 | e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
368+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350369 | e @ FindReviewerError :: ReviewerOffRotation { .. }
351370 | e @ FindReviewerError :: DatabaseError ( _)
352371 | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,8 +387,7 @@ async fn determine_assignee(
368387
369388 if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370389 match find_reviewer_from_names (
371- & db_client,
372- ctx. workqueue . clone ( ) ,
390+ ctx,
373391 & teams,
374392 config,
375393 & event. issue ,
@@ -551,10 +569,8 @@ pub(super) async fn handle_command(
551569 return Ok ( ( ) ) ;
552570 }
553571
554- let db_client = ctx. db . get ( ) . await ;
555572 let assignee = match find_reviewer_from_names (
556- & db_client,
557- ctx. workqueue . clone ( ) ,
573+ ctx,
558574 & teams,
559575 config,
560576 issue,
@@ -569,7 +585,7 @@ pub(super) async fn handle_command(
569585 }
570586 } ;
571587
572- set_assignee ( issue, & ctx. github , & assignee) . await ;
588+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573589 } else {
574590 let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575591
@@ -680,6 +696,8 @@ enum FindReviewerError {
680696 ReviewerIsPrAuthor { username : String } ,
681697 /// Requested reviewer is already assigned to that PR
682698 ReviewerAlreadyAssigned { username : String } ,
699+ /// Requested reviewer is already assigned previously to that PR.
700+ ReviewerPreviouslyAssigned { username : String } ,
683701 /// Data required for assignment could not be loaded from the DB.
684702 DatabaseError ( String ) ,
685703 /// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +744,13 @@ impl fmt::Display for FindReviewerError {
726744 REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727745 )
728746 }
747+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
748+ write ! (
749+ f,
750+ "{}" ,
751+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
752+ )
753+ }
729754 FindReviewerError :: DatabaseError ( error) => {
730755 write ! ( f, "Database error: {error}" )
731756 }
@@ -748,8 +773,7 @@ Please select a different reviewer.",
748773/// auto-assign groups, or rust-lang team names. It must have at least one
749774/// entry.
750775async fn find_reviewer_from_names (
751- db : & DbClient ,
752- workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
776+ ctx : & Context ,
753777 teams : & Teams ,
754778 config : & AssignConfig ,
755779 issue : & Issue ,
@@ -763,7 +787,7 @@ async fn find_reviewer_from_names(
763787 }
764788
765789 let candidates =
766- candidate_reviewers_from_names ( db , workqueue , teams, config, issue, names) . await ?;
790+ candidate_reviewers_from_names ( ctx , teams, config, issue, names) . await ?;
767791 assert ! ( !candidates. is_empty( ) ) ;
768792
769793 // This uses a relatively primitive random choice algorithm.
@@ -916,15 +940,15 @@ fn expand_teams_and_groups(
916940/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917941/// If no reviewer is available, returns an error.
918942async fn candidate_reviewers_from_names < ' a > (
919- db : & DbClient ,
920- workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
943+ ctx : & Context ,
921944 teams : & ' a Teams ,
922945 config : & ' a AssignConfig ,
923946 issue : & Issue ,
924947 names : & ' a [ String ] ,
925948) -> Result < HashSet < String > , FindReviewerError > {
926949 // Step 1: expand teams and groups into candidate names
927950 let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
951+ let expansion_happend = expanded. iter ( ) . any ( |c| c. origin == ReviewerCandidateOrigin :: Expanded ) ;
928952 let expanded_count = expanded. len ( ) ;
929953
930954 // Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +961,7 @@ async fn candidate_reviewers_from_names<'a>(
937961 // Set of candidate usernames to choose from.
938962 // We go through each expanded candidate and store either success or an error for them.
939963 let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
964+ let previous_reviewer_names = get_previous_reviewer_names ( ctx, issue) . await ;
940965
941966 // Step 2: pre-filter candidates based on checks that we can perform quickly
942967 for reviewer_candidate in expanded {
@@ -949,6 +974,8 @@ async fn candidate_reviewers_from_names<'a>(
949974 . iter ( )
950975 . any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951976
977+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
978+
952979 // Record the reason why the candidate was filtered out
953980 let reason = {
954981 if is_pr_author {
@@ -963,6 +990,12 @@ async fn candidate_reviewers_from_names<'a>(
963990 Some ( FindReviewerError :: ReviewerAlreadyAssigned {
964991 username : candidate. clone ( ) ,
965992 } )
993+ } else if expansion_happend && is_previously_assigned {
994+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
995+ // `r? @reviewer` will not consider the reviewer previously assigned
996+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
997+ username : candidate. clone ( ) ,
998+ } )
966999 } else {
9671000 None
9681001 }
@@ -983,12 +1016,14 @@ async fn candidate_reviewers_from_names<'a>(
9831016 . filter_map ( |res| res. as_deref ( ) . ok ( ) . map ( |s| s. to_string ( ) ) )
9841017 . collect ( ) ;
9851018 let usernames: Vec < & str > = usernames. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
986- let review_prefs = get_review_prefs_batch ( db, & usernames)
1019+
1020+ let db_client = ctx. db . get ( ) . await ;
1021+ let review_prefs = get_review_prefs_batch ( & db_client, & usernames)
9871022 . await
9881023 . context ( "cannot fetch review preferences" )
9891024 . map_err ( |e| FindReviewerError :: DatabaseError ( e. to_string ( ) ) ) ?;
9901025
991- let workqueue = workqueue. read ( ) . await ;
1026+ let workqueue = ctx . workqueue . read ( ) . await ;
9921027
9931028 // Step 4: check review preferences
9941029 candidates = candidates
@@ -1009,9 +1044,6 @@ async fn candidate_reviewers_from_names<'a>(
10091044 return Err ( FindReviewerError :: ReviewerAtMaxCapacity { username } ) ;
10101045 }
10111046 }
1012- if review_prefs. rotation_mode == RotationMode :: OffRotation {
1013- return Err ( FindReviewerError :: ReviewerOffRotation { username } ) ;
1014- }
10151047
10161048 return Ok ( username) ;
10171049 } )
@@ -1058,3 +1090,14 @@ async fn candidate_reviewers_from_names<'a>(
10581090 . collect ( ) )
10591091 }
10601092}
1093+
1094+ async fn get_previous_reviewer_names ( ctx : & Context , issue : & Issue ) -> HashSet < String > {
1095+ let mut db = ctx. db . get ( ) . await ;
1096+ let state: IssueData < ' _ , Reviewers > =
1097+ match IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await {
1098+ Ok ( state) => state,
1099+ Err ( _) => return HashSet :: new ( ) ,
1100+ } ;
1101+
1102+ state. data . names
1103+ }
0 commit comments