20
20
//! `assign.owners` config, it will auto-select an assignee based on the files
21
21
//! the PR modifies.
22
22
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;
24
25
use crate :: github:: UserId ;
25
- use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
26
26
use crate :: {
27
27
config:: AssignConfig ,
28
28
github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
@@ -36,9 +36,6 @@ use rand::seq::IteratorRandom;
36
36
use rust_team_data:: v1:: Teams ;
37
37
use std:: collections:: { HashMap , HashSet } ;
38
38
use std:: fmt;
39
- use std:: sync:: Arc ;
40
- use tokio:: sync:: RwLock ;
41
- use tokio_postgres:: Client as DbClient ;
42
39
use tracing as log;
43
40
44
41
#[ cfg( test) ]
@@ -92,9 +89,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
92
89
93
90
Please choose another assignee." ;
94
91
92
+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewers are assigned before.
93
+
94
+ Please choose another assignee by using `r? @reviewer`." ;
95
+
95
96
// Special account that we use to prevent assignment.
96
97
const GHOST_ACCOUNT : & str = "ghost" ;
97
98
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
+
98
109
/// Assignment data stored in the issue/PR body.
99
110
#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
100
111
struct AssignData {
@@ -217,7 +228,7 @@ pub(super) async fn handle_input(
217
228
None
218
229
} ;
219
230
if let Some ( assignee) = assignee {
220
- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
231
+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
221
232
}
222
233
223
234
if let Some ( welcome) = welcome {
@@ -249,15 +260,19 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249
260
}
250
261
251
262
/// 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
+
253
268
// Don't re-assign if already assigned, e.g. on comment edit
254
269
if issue. contain_assignee ( & username) {
255
270
log:: trace!(
256
271
"ignoring assign PR {} to {}, already assigned" ,
257
272
issue. global_id( ) ,
258
273
username,
259
274
) ;
260
- return ;
275
+ return Ok ( ( ) ) ;
261
276
}
262
277
if let Err ( err) = issue. set_assignee ( github, & username) . await {
263
278
log:: warn!(
@@ -280,8 +295,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280
295
. await
281
296
{
282
297
log:: warn!( "failed to post error comment: {e}" ) ;
298
+ return Err ( e) ;
283
299
}
284
300
}
301
+
302
+ // Record the reviewer in the database
303
+ state. data . names . insert ( username. to_string ( ) ) ;
304
+ state. save ( ) . await ?;
305
+ Ok ( ( ) )
285
306
}
286
307
287
308
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,13 +321,11 @@ async fn determine_assignee(
300
321
config : & AssignConfig ,
301
322
diff : & [ FileDiff ] ,
302
323
) -> anyhow:: Result < ( Option < String > , bool ) > {
303
- let db_client = ctx. db . get ( ) . await ;
304
324
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305
325
if let Some ( name) = assign_command {
306
326
// User included `r?` in the opening PR body.
307
327
match find_reviewer_from_names (
308
- & db_client,
309
- ctx. workqueue . clone ( ) ,
328
+ ctx,
310
329
& teams,
311
330
config,
312
331
& event. issue ,
@@ -328,8 +347,7 @@ async fn determine_assignee(
328
347
match find_reviewers_from_diff ( config, diff) {
329
348
Ok ( candidates) if !candidates. is_empty ( ) => {
330
349
match find_reviewer_from_names (
331
- & db_client,
332
- ctx. workqueue . clone ( ) ,
350
+ ctx,
333
351
& teams,
334
352
config,
335
353
& event. issue ,
@@ -347,6 +365,7 @@ async fn determine_assignee(
347
365
e @ FindReviewerError :: NoReviewer { .. }
348
366
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349
367
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
368
+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350
369
| e @ FindReviewerError :: ReviewerOffRotation { .. }
351
370
| e @ FindReviewerError :: DatabaseError ( _)
352
371
| e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,8 +387,7 @@ async fn determine_assignee(
368
387
369
388
if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370
389
match find_reviewer_from_names (
371
- & db_client,
372
- ctx. workqueue . clone ( ) ,
390
+ ctx,
373
391
& teams,
374
392
config,
375
393
& event. issue ,
@@ -551,10 +569,8 @@ pub(super) async fn handle_command(
551
569
return Ok ( ( ) ) ;
552
570
}
553
571
554
- let db_client = ctx. db . get ( ) . await ;
555
572
let assignee = match find_reviewer_from_names (
556
- & db_client,
557
- ctx. workqueue . clone ( ) ,
573
+ ctx,
558
574
& teams,
559
575
config,
560
576
issue,
@@ -569,7 +585,7 @@ pub(super) async fn handle_command(
569
585
}
570
586
} ;
571
587
572
- set_assignee ( issue, & ctx. github , & assignee) . await ;
588
+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573
589
} else {
574
590
let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575
591
@@ -680,6 +696,8 @@ enum FindReviewerError {
680
696
ReviewerIsPrAuthor { username : String } ,
681
697
/// Requested reviewer is already assigned to that PR
682
698
ReviewerAlreadyAssigned { username : String } ,
699
+ /// Requested reviewer is already assigned previously to that PR.
700
+ ReviewerPreviouslyAssigned { username : String } ,
683
701
/// Data required for assignment could not be loaded from the DB.
684
702
DatabaseError ( String ) ,
685
703
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +744,13 @@ impl fmt::Display for FindReviewerError {
726
744
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727
745
)
728
746
}
747
+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
748
+ write ! (
749
+ f,
750
+ "{}" ,
751
+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
752
+ )
753
+ }
729
754
FindReviewerError :: DatabaseError ( error) => {
730
755
write ! ( f, "Database error: {error}" )
731
756
}
@@ -748,8 +773,7 @@ Please select a different reviewer.",
748
773
/// auto-assign groups, or rust-lang team names. It must have at least one
749
774
/// entry.
750
775
async fn find_reviewer_from_names (
751
- db : & DbClient ,
752
- workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
776
+ ctx : & Context ,
753
777
teams : & Teams ,
754
778
config : & AssignConfig ,
755
779
issue : & Issue ,
@@ -763,7 +787,7 @@ async fn find_reviewer_from_names(
763
787
}
764
788
765
789
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 ?;
767
791
assert ! ( !candidates. is_empty( ) ) ;
768
792
769
793
// This uses a relatively primitive random choice algorithm.
@@ -916,15 +940,15 @@ fn expand_teams_and_groups(
916
940
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917
941
/// If no reviewer is available, returns an error.
918
942
async fn candidate_reviewers_from_names < ' a > (
919
- db : & DbClient ,
920
- workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
943
+ ctx : & Context ,
921
944
teams : & ' a Teams ,
922
945
config : & ' a AssignConfig ,
923
946
issue : & Issue ,
924
947
names : & ' a [ String ] ,
925
948
) -> Result < HashSet < String > , FindReviewerError > {
926
949
// Step 1: expand teams and groups into candidate names
927
950
let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
951
+ let expansion_happend = expanded. iter ( ) . any ( |c| c. origin == ReviewerCandidateOrigin :: Expanded ) ;
928
952
let expanded_count = expanded. len ( ) ;
929
953
930
954
// Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +961,7 @@ async fn candidate_reviewers_from_names<'a>(
937
961
// Set of candidate usernames to choose from.
938
962
// We go through each expanded candidate and store either success or an error for them.
939
963
let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
964
+ let previous_reviewer_names = get_previous_reviewer_names ( ctx, issue) . await ;
940
965
941
966
// Step 2: pre-filter candidates based on checks that we can perform quickly
942
967
for reviewer_candidate in expanded {
@@ -949,6 +974,8 @@ async fn candidate_reviewers_from_names<'a>(
949
974
. iter ( )
950
975
. any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951
976
977
+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
978
+
952
979
// Record the reason why the candidate was filtered out
953
980
let reason = {
954
981
if is_pr_author {
@@ -963,6 +990,12 @@ async fn candidate_reviewers_from_names<'a>(
963
990
Some ( FindReviewerError :: ReviewerAlreadyAssigned {
964
991
username : candidate. clone ( ) ,
965
992
} )
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
+ } )
966
999
} else {
967
1000
None
968
1001
}
@@ -983,12 +1016,14 @@ async fn candidate_reviewers_from_names<'a>(
983
1016
. filter_map ( |res| res. as_deref ( ) . ok ( ) . map ( |s| s. to_string ( ) ) )
984
1017
. collect ( ) ;
985
1018
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)
987
1022
. await
988
1023
. context ( "cannot fetch review preferences" )
989
1024
. map_err ( |e| FindReviewerError :: DatabaseError ( e. to_string ( ) ) ) ?;
990
1025
991
- let workqueue = workqueue. read ( ) . await ;
1026
+ let workqueue = ctx . workqueue . read ( ) . await ;
992
1027
993
1028
// Step 4: check review preferences
994
1029
candidates = candidates
@@ -1009,9 +1044,6 @@ async fn candidate_reviewers_from_names<'a>(
1009
1044
return Err ( FindReviewerError :: ReviewerAtMaxCapacity { username } ) ;
1010
1045
}
1011
1046
}
1012
- if review_prefs. rotation_mode == RotationMode :: OffRotation {
1013
- return Err ( FindReviewerError :: ReviewerOffRotation { username } ) ;
1014
- }
1015
1047
1016
1048
return Ok ( username) ;
1017
1049
} )
@@ -1058,3 +1090,14 @@ async fn candidate_reviewers_from_names<'a>(
1058
1090
. collect ( ) )
1059
1091
}
1060
1092
}
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