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:: issue_data:: IssueData ;
23
24
use crate :: db:: review_prefs:: { get_review_prefs_batch, RotationMode } ;
24
25
use crate :: github:: UserId ;
25
26
use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
@@ -92,9 +93,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
92
93
93
94
Please choose another assignee." ;
94
95
96
+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewers are assigned before.
97
+
98
+ Please choose another assignee by using `r? @reviewer`." ;
99
+
95
100
// Special account that we use to prevent assignment.
96
101
const GHOST_ACCOUNT : & str = "ghost" ;
97
102
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
+
98
113
/// Assignment data stored in the issue/PR body.
99
114
#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
100
115
struct AssignData {
@@ -217,7 +232,7 @@ pub(super) async fn handle_input(
217
232
None
218
233
} ;
219
234
if let Some ( assignee) = assignee {
220
- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
235
+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
221
236
}
222
237
223
238
if let Some ( welcome) = welcome {
@@ -249,15 +264,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249
264
}
250
265
251
266
/// 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
+
253
277
// Don't re-assign if already assigned, e.g. on comment edit
254
278
if issue. contain_assignee ( & username) {
255
279
log:: trace!(
256
280
"ignoring assign PR {} to {}, already assigned" ,
257
281
issue. global_id( ) ,
258
282
username,
259
283
) ;
260
- return ;
284
+ return Ok ( ( ) ) ;
261
285
}
262
286
if let Err ( err) = issue. set_assignee ( github, & username) . await {
263
287
log:: warn!(
@@ -280,8 +304,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280
304
. await
281
305
{
282
306
log:: warn!( "failed to post error comment: {e}" ) ;
307
+ return Err ( e) ;
283
308
}
284
309
}
310
+
311
+ // Record the reviewer in the database
312
+ state. data . names . insert ( username. to_string ( ) ) ;
313
+ state. save ( ) . await ?;
314
+ Ok ( ( ) )
285
315
}
286
316
287
317
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +330,12 @@ async fn determine_assignee(
300
330
config : & AssignConfig ,
301
331
diff : & [ FileDiff ] ,
302
332
) -> anyhow:: Result < ( Option < String > , bool ) > {
303
- let db_client = ctx. db . get ( ) . await ;
333
+ let mut db_client = ctx. db . get ( ) . await ;
304
334
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
305
335
if let Some ( name) = assign_command {
306
336
// User included `r?` in the opening PR body.
307
337
match find_reviewer_from_names (
308
- & db_client,
338
+ & mut db_client,
309
339
ctx. workqueue . clone ( ) ,
310
340
& teams,
311
341
config,
@@ -328,7 +358,7 @@ async fn determine_assignee(
328
358
match find_reviewers_from_diff ( config, diff) {
329
359
Ok ( candidates) if !candidates. is_empty ( ) => {
330
360
match find_reviewer_from_names (
331
- & db_client,
361
+ & mut db_client,
332
362
ctx. workqueue . clone ( ) ,
333
363
& teams,
334
364
config,
@@ -347,6 +377,7 @@ async fn determine_assignee(
347
377
e @ FindReviewerError :: NoReviewer { .. }
348
378
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
349
379
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
380
+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. }
350
381
| e @ FindReviewerError :: ReviewerOffRotation { .. }
351
382
| e @ FindReviewerError :: DatabaseError ( _)
352
383
| e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
@@ -368,7 +399,7 @@ async fn determine_assignee(
368
399
369
400
if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
370
401
match find_reviewer_from_names (
371
- & db_client,
402
+ & mut db_client,
372
403
ctx. workqueue . clone ( ) ,
373
404
& teams,
374
405
config,
@@ -550,10 +581,9 @@ pub(super) async fn handle_command(
550
581
issue. remove_assignees ( & ctx. github , Selection :: All ) . await ?;
551
582
return Ok ( ( ) ) ;
552
583
}
553
-
554
- let db_client = ctx. db . get ( ) . await ;
584
+ let mut db_client = ctx. db . get ( ) . await ;
555
585
let assignee = match find_reviewer_from_names (
556
- & db_client,
586
+ & mut db_client,
557
587
ctx. workqueue . clone ( ) ,
558
588
& teams,
559
589
config,
@@ -569,7 +599,7 @@ pub(super) async fn handle_command(
569
599
}
570
600
} ;
571
601
572
- set_assignee ( issue, & ctx. github , & assignee) . await ;
602
+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
573
603
} else {
574
604
let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
575
605
@@ -680,6 +710,8 @@ enum FindReviewerError {
680
710
ReviewerIsPrAuthor { username : String } ,
681
711
/// Requested reviewer is already assigned to that PR
682
712
ReviewerAlreadyAssigned { username : String } ,
713
+ /// Requested reviewer is already assigned previously to that PR.
714
+ ReviewerPreviouslyAssigned { username : String } ,
683
715
/// Data required for assignment could not be loaded from the DB.
684
716
DatabaseError ( String ) ,
685
717
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +758,13 @@ impl fmt::Display for FindReviewerError {
726
758
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
727
759
)
728
760
}
761
+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
762
+ write ! (
763
+ f,
764
+ "{}" ,
765
+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
766
+ )
767
+ }
729
768
FindReviewerError :: DatabaseError ( error) => {
730
769
write ! ( f, "Database error: {error}" )
731
770
}
@@ -748,7 +787,7 @@ Please select a different reviewer.",
748
787
/// auto-assign groups, or rust-lang team names. It must have at least one
749
788
/// entry.
750
789
async fn find_reviewer_from_names (
751
- db : & DbClient ,
790
+ db : & mut DbClient ,
752
791
workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
753
792
teams : & Teams ,
754
793
config : & AssignConfig ,
@@ -916,7 +955,7 @@ fn expand_teams_and_groups(
916
955
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917
956
/// If no reviewer is available, returns an error.
918
957
async fn candidate_reviewers_from_names < ' a > (
919
- db : & DbClient ,
958
+ db : & mut DbClient ,
920
959
workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
921
960
teams : & ' a Teams ,
922
961
config : & ' a AssignConfig ,
@@ -925,6 +964,9 @@ async fn candidate_reviewers_from_names<'a>(
925
964
) -> Result < HashSet < String > , FindReviewerError > {
926
965
// Step 1: expand teams and groups into candidate names
927
966
let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
967
+ let expansion_happend = expanded
968
+ . iter ( )
969
+ . any ( |c| c. origin == ReviewerCandidateOrigin :: Expanded ) ;
928
970
let expanded_count = expanded. len ( ) ;
929
971
930
972
// Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +979,7 @@ async fn candidate_reviewers_from_names<'a>(
937
979
// Set of candidate usernames to choose from.
938
980
// We go through each expanded candidate and store either success or an error for them.
939
981
let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
982
+ let previous_reviewer_names = get_previous_reviewer_names ( db, issue) . await ;
940
983
941
984
// Step 2: pre-filter candidates based on checks that we can perform quickly
942
985
for reviewer_candidate in expanded {
@@ -949,6 +992,8 @@ async fn candidate_reviewers_from_names<'a>(
949
992
. iter ( )
950
993
. any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
951
994
995
+ let is_previously_assigned = previous_reviewer_names. contains ( & reviewer_candidate. name ) ;
996
+
952
997
// Record the reason why the candidate was filtered out
953
998
let reason = {
954
999
if is_pr_author {
@@ -963,6 +1008,12 @@ async fn candidate_reviewers_from_names<'a>(
963
1008
Some ( FindReviewerError :: ReviewerAlreadyAssigned {
964
1009
username : candidate. clone ( ) ,
965
1010
} )
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
+ } )
966
1017
} else {
967
1018
None
968
1019
}
@@ -1058,3 +1109,13 @@ async fn candidate_reviewers_from_names<'a>(
1058
1109
. collect ( ) )
1059
1110
}
1060
1111
}
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