22
22
23
23
use crate :: {
24
24
config:: AssignConfig ,
25
+ db:: issue_data:: IssueData ,
25
26
github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
26
27
handlers:: { Context , GithubClient , IssuesEvent } ,
27
28
interactions:: EditIssueBody ,
@@ -33,7 +34,6 @@ use rand::seq::IteratorRandom;
33
34
use rust_team_data:: v1:: Teams ;
34
35
use std:: collections:: { HashMap , HashSet } ;
35
36
use std:: fmt;
36
- use tokio_postgres:: Client as DbClient ;
37
37
use tracing as log;
38
38
39
39
#[ cfg( test) ]
@@ -87,9 +87,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
87
87
88
88
Please choose another assignee." ;
89
89
90
+ const REVIEWER_ASSIGNED_BEFORE : & str = "Requested reviewers are assigned before.
91
+
92
+ Please choose another assignee by using `r? @reviewer`." ;
93
+
90
94
// Special account that we use to prevent assignment.
91
95
const GHOST_ACCOUNT : & str = "ghost" ;
92
96
97
+ /// Key for the state in the database
98
+ const PREVIOUS_REVIEWER_KEY : & str = "previous-reviewer" ;
99
+
100
+ /// State stored in the database
101
+ #[ derive( Debug , Default , serde:: Deserialize , serde:: Serialize ) ]
102
+ struct Reviewer {
103
+ /// List of the last warnings in the most recent comment.
104
+ names : HashSet < String > ,
105
+ }
106
+
93
107
#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
94
108
struct AssignData {
95
109
user : Option < String > ,
@@ -179,7 +193,7 @@ pub(super) async fn handle_input(
179
193
None
180
194
} ;
181
195
if let Some ( assignee) = assignee {
182
- set_assignee ( & event. issue , & ctx. github , & assignee) . await ;
196
+ set_assignee ( & ctx , & event. issue , & ctx. github , & assignee) . await ? ;
183
197
}
184
198
185
199
if let Some ( welcome) = welcome {
@@ -211,15 +225,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
211
225
}
212
226
213
227
/// Sets the assignee of a PR, alerting any errors.
214
- async fn set_assignee ( issue : & Issue , github : & GithubClient , username : & str ) {
228
+ async fn set_assignee (
229
+ ctx : & Context ,
230
+ issue : & Issue ,
231
+ github : & GithubClient ,
232
+ username : & str ,
233
+ ) -> anyhow:: Result < ( ) > {
234
+ let mut db = ctx. db . get ( ) . await ;
235
+ let mut state: IssueData < ' _ , Reviewer > =
236
+ IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await ?;
237
+
215
238
// Don't re-assign if already assigned, e.g. on comment edit
216
239
if issue. contain_assignee ( & username) {
217
240
log:: trace!(
218
241
"ignoring assign PR {} to {}, already assigned" ,
219
242
issue. global_id( ) ,
220
243
username,
221
244
) ;
222
- return ;
245
+ return Ok ( ( ) ) ;
223
246
}
224
247
if let Err ( err) = issue. set_assignee ( github, & username) . await {
225
248
log:: warn!(
@@ -242,8 +265,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
242
265
. await
243
266
{
244
267
log:: warn!( "failed to post error comment: {e}" ) ;
268
+ return Err ( e) ;
245
269
}
246
270
}
271
+
272
+ state. data . names . insert ( username. to_string ( ) ) ;
273
+ state. save ( ) . await ?;
274
+
275
+ Ok ( ( ) )
247
276
}
248
277
249
278
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -261,11 +290,10 @@ async fn determine_assignee(
261
290
config : & AssignConfig ,
262
291
diff : & [ FileDiff ] ,
263
292
) -> anyhow:: Result < ( Option < String > , bool ) > {
264
- let db_client = ctx. db . get ( ) . await ;
265
293
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
266
294
if let Some ( name) = find_assign_command ( ctx, event) {
267
295
// User included `r?` in the opening PR body.
268
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , & [ name] ) . await {
296
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , & [ name] ) . await {
269
297
Ok ( assignee) => return Ok ( ( Some ( assignee) , true ) ) ,
270
298
Err ( e) => {
271
299
event
@@ -279,7 +307,7 @@ async fn determine_assignee(
279
307
// Errors fall-through to try fallback group.
280
308
match find_reviewers_from_diff ( config, diff) {
281
309
Ok ( candidates) if !candidates. is_empty ( ) => {
282
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , & candidates)
310
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , & candidates)
283
311
. await
284
312
{
285
313
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
@@ -290,9 +318,11 @@ async fn determine_assignee(
290
318
) ,
291
319
Err (
292
320
e @ FindReviewerError :: NoReviewer { .. }
321
+ // TODO: only NoReviewer can be reached here!
293
322
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
294
323
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
295
- | e @ FindReviewerError :: ReviewerOnVacation { .. } ,
324
+ | e @ FindReviewerError :: ReviewerOnVacation { .. }
325
+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. } ,
296
326
) => log:: trace!(
297
327
"no reviewer could be determined for PR {}: {e}" ,
298
328
event. issue. global_id( )
@@ -310,7 +340,7 @@ async fn determine_assignee(
310
340
}
311
341
312
342
if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
313
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , fallback) . await {
343
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , fallback) . await {
314
344
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
315
345
Err ( e) => {
316
346
log:: trace!(
@@ -485,24 +515,18 @@ pub(super) async fn handle_command(
485
515
return Ok ( ( ) ) ;
486
516
}
487
517
488
- let db_client = ctx. db . get ( ) . await ;
489
- let assignee = match find_reviewer_from_names (
490
- & db_client,
491
- & teams,
492
- config,
493
- issue,
494
- & [ assignee. to_string ( ) ] ,
495
- )
496
- . await
497
- {
498
- Ok ( assignee) => assignee,
499
- Err ( e) => {
500
- issue. post_comment ( & ctx. github , & e. to_string ( ) ) . await ?;
501
- return Ok ( ( ) ) ;
502
- }
503
- } ;
518
+ let assignee =
519
+ match find_reviewer_from_names ( ctx, & teams, config, issue, & [ assignee. to_string ( ) ] )
520
+ . await
521
+ {
522
+ Ok ( assignee) => assignee,
523
+ Err ( e) => {
524
+ issue. post_comment ( & ctx. github , & e. to_string ( ) ) . await ?;
525
+ return Ok ( ( ) ) ;
526
+ }
527
+ } ;
504
528
505
- set_assignee ( issue, & ctx. github , & assignee) . await ;
529
+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
506
530
} else {
507
531
let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
508
532
@@ -612,6 +636,8 @@ pub enum FindReviewerError {
612
636
ReviewerIsPrAuthor { username : String } ,
613
637
/// Requested reviewer is already assigned to that PR
614
638
ReviewerAlreadyAssigned { username : String } ,
639
+ /// Requested reviewer is already assigned previously to that PR
640
+ ReviewerPreviouslyAssigned { username : String } ,
615
641
}
616
642
617
643
impl std:: error:: Error for FindReviewerError { }
@@ -654,6 +680,13 @@ impl fmt::Display for FindReviewerError {
654
680
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
655
681
)
656
682
}
683
+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
684
+ write ! (
685
+ f,
686
+ "{}" ,
687
+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
688
+ )
689
+ }
657
690
}
658
691
}
659
692
}
@@ -665,7 +698,7 @@ impl fmt::Display for FindReviewerError {
665
698
/// auto-assign groups, or rust-lang team names. It must have at least one
666
699
/// entry.
667
700
async fn find_reviewer_from_names (
668
- _db : & DbClient ,
701
+ ctx : & Context ,
669
702
teams : & Teams ,
670
703
config : & AssignConfig ,
671
704
issue : & Issue ,
@@ -678,7 +711,7 @@ async fn find_reviewer_from_names(
678
711
}
679
712
}
680
713
681
- let candidates = candidate_reviewers_from_names ( teams, config, issue, names) ?;
714
+ let candidates = candidate_reviewers_from_names ( ctx , teams, config, issue, names) . await ?;
682
715
// This uses a relatively primitive random choice algorithm.
683
716
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
684
717
//
@@ -782,7 +815,8 @@ fn expand_teams_and_groups(
782
815
783
816
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
784
817
/// If not reviewer is available, returns an error.
785
- fn candidate_reviewers_from_names < ' a > (
818
+ async fn candidate_reviewers_from_names < ' a > (
819
+ ctx : & Context ,
786
820
teams : & ' a Teams ,
787
821
config : & ' a AssignConfig ,
788
822
issue : & Issue ,
@@ -804,6 +838,9 @@ fn candidate_reviewers_from_names<'a>(
804
838
. iter ( )
805
839
. any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
806
840
841
+ let previous_reviewer_names = get_previous_reviewer_names ( ctx, issue) . await ;
842
+ let is_previously_assigned = previous_reviewer_names. contains ( & candidate) ;
843
+
807
844
// Record the reason why the candidate was filtered out
808
845
let reason = {
809
846
if is_pr_author {
@@ -818,6 +855,12 @@ fn candidate_reviewers_from_names<'a>(
818
855
Some ( FindReviewerError :: ReviewerAlreadyAssigned {
819
856
username : candidate. clone ( ) ,
820
857
} )
858
+ } else if expansion_happened && is_previously_assigned {
859
+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
860
+ // `r? @reviewer` will not consider the reviewer previously assigned
861
+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
862
+ username : candidate. clone ( ) ,
863
+ } )
821
864
} else {
822
865
None
823
866
}
@@ -863,3 +906,19 @@ fn candidate_reviewers_from_names<'a>(
863
906
Ok ( valid_candidates)
864
907
}
865
908
}
909
+
910
+ async fn get_previous_reviewer_names ( ctx : & Context , issue : & Issue ) -> HashSet < String > {
911
+ if cfg ! ( test) {
912
+ return HashSet :: new ( ) ;
913
+ }
914
+
915
+ // Get the state of the warnings for this PR in the database.
916
+ let mut db = ctx. db . get ( ) . await ;
917
+ let state: IssueData < ' _ , Reviewer > =
918
+ match IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await {
919
+ Ok ( state) => state,
920
+ Err ( _) => return HashSet :: new ( ) ,
921
+ } ;
922
+
923
+ state. data . names
924
+ }
0 commit comments