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,12 @@ 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
+ state. data . names . insert ( username. to_string ( ) ) ;
272
+ state. save ( ) . await ?;
273
+ Ok ( ( ) )
247
274
}
248
275
249
276
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -261,11 +288,10 @@ async fn determine_assignee(
261
288
config : & AssignConfig ,
262
289
diff : & [ FileDiff ] ,
263
290
) -> anyhow:: Result < ( Option < String > , bool ) > {
264
- let db_client = ctx. db . get ( ) . await ;
265
291
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
266
292
if let Some ( name) = find_assign_command ( ctx, event) {
267
293
// User included `r?` in the opening PR body.
268
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , & [ name] ) . await {
294
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , & [ name] ) . await {
269
295
Ok ( assignee) => return Ok ( ( Some ( assignee) , true ) ) ,
270
296
Err ( e) => {
271
297
event
@@ -279,7 +305,7 @@ async fn determine_assignee(
279
305
// Errors fall-through to try fallback group.
280
306
match find_reviewers_from_diff ( config, diff) {
281
307
Ok ( candidates) if !candidates. is_empty ( ) => {
282
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , & candidates)
308
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , & candidates)
283
309
. await
284
310
{
285
311
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
@@ -290,9 +316,11 @@ async fn determine_assignee(
290
316
) ,
291
317
Err (
292
318
e @ FindReviewerError :: NoReviewer { .. }
319
+ // TODO: only NoReviewer can be reached here!
293
320
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
294
321
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
295
- | e @ FindReviewerError :: ReviewerOnVacation { .. } ,
322
+ | e @ FindReviewerError :: ReviewerOnVacation { .. }
323
+ | e @ FindReviewerError :: ReviewerPreviouslyAssigned { .. } ,
296
324
) => log:: trace!(
297
325
"no reviewer could be determined for PR {}: {e}" ,
298
326
event. issue. global_id( )
@@ -310,7 +338,7 @@ async fn determine_assignee(
310
338
}
311
339
312
340
if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
313
- match find_reviewer_from_names ( & db_client , & teams, config, & event. issue , fallback) . await {
341
+ match find_reviewer_from_names ( & ctx , & teams, config, & event. issue , fallback) . await {
314
342
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
315
343
Err ( e) => {
316
344
log:: trace!(
@@ -485,24 +513,18 @@ pub(super) async fn handle_command(
485
513
return Ok ( ( ) ) ;
486
514
}
487
515
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
- } ;
516
+ let assignee =
517
+ match find_reviewer_from_names ( ctx, & teams, config, issue, & [ assignee. to_string ( ) ] )
518
+ . await
519
+ {
520
+ Ok ( assignee) => assignee,
521
+ Err ( e) => {
522
+ issue. post_comment ( & ctx. github , & e. to_string ( ) ) . await ?;
523
+ return Ok ( ( ) ) ;
524
+ }
525
+ } ;
504
526
505
- set_assignee ( issue, & ctx. github , & assignee) . await ;
527
+ set_assignee ( ctx , issue, & ctx. github , & assignee) . await ? ;
506
528
} else {
507
529
let e = EditIssueBody :: new ( & issue, "ASSIGN" ) ;
508
530
@@ -612,6 +634,8 @@ pub enum FindReviewerError {
612
634
ReviewerIsPrAuthor { username : String } ,
613
635
/// Requested reviewer is already assigned to that PR
614
636
ReviewerAlreadyAssigned { username : String } ,
637
+ /// Requested reviewer is already assigned previously to that PR
638
+ ReviewerPreviouslyAssigned { username : String } ,
615
639
}
616
640
617
641
impl std:: error:: Error for FindReviewerError { }
@@ -654,6 +678,13 @@ impl fmt::Display for FindReviewerError {
654
678
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
655
679
)
656
680
}
681
+ FindReviewerError :: ReviewerPreviouslyAssigned { username } => {
682
+ write ! (
683
+ f,
684
+ "{}" ,
685
+ REVIEWER_ASSIGNED_BEFORE . replace( "{username}" , username)
686
+ )
687
+ }
657
688
}
658
689
}
659
690
}
@@ -665,7 +696,7 @@ impl fmt::Display for FindReviewerError {
665
696
/// auto-assign groups, or rust-lang team names. It must have at least one
666
697
/// entry.
667
698
async fn find_reviewer_from_names (
668
- _db : & DbClient ,
699
+ ctx : & Context ,
669
700
teams : & Teams ,
670
701
config : & AssignConfig ,
671
702
issue : & Issue ,
@@ -678,7 +709,7 @@ async fn find_reviewer_from_names(
678
709
}
679
710
}
680
711
681
- let candidates = candidate_reviewers_from_names ( teams, config, issue, names) ?;
712
+ let candidates = candidate_reviewers_from_names ( ctx , teams, config, issue, names) . await ?;
682
713
// This uses a relatively primitive random choice algorithm.
683
714
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
684
715
//
@@ -782,7 +813,8 @@ fn expand_teams_and_groups(
782
813
783
814
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
784
815
/// If not reviewer is available, returns an error.
785
- fn candidate_reviewers_from_names < ' a > (
816
+ async fn candidate_reviewers_from_names < ' a > (
817
+ ctx : & Context ,
786
818
teams : & ' a Teams ,
787
819
config : & ' a AssignConfig ,
788
820
issue : & Issue ,
@@ -804,6 +836,9 @@ fn candidate_reviewers_from_names<'a>(
804
836
. iter ( )
805
837
. any ( |assignee| name_lower == assignee. login . to_lowercase ( ) ) ;
806
838
839
+ let previous_reviewer_names = get_previous_reviewer_names ( ctx, issue) . await ;
840
+ let is_previously_assigned = previous_reviewer_names. contains ( & candidate) ;
841
+
807
842
// Record the reason why the candidate was filtered out
808
843
let reason = {
809
844
if is_pr_author {
@@ -818,6 +853,12 @@ fn candidate_reviewers_from_names<'a>(
818
853
Some ( FindReviewerError :: ReviewerAlreadyAssigned {
819
854
username : candidate. clone ( ) ,
820
855
} )
856
+ } else if expansion_happened && is_previously_assigned {
857
+ // **Only** when r? group is expanded, we consider the reviewer previously assigned
858
+ // `r? @reviewer` will not consider the reviewer previously assigned
859
+ Some ( FindReviewerError :: ReviewerPreviouslyAssigned {
860
+ username : candidate. clone ( ) ,
861
+ } )
821
862
} else {
822
863
None
823
864
}
@@ -863,3 +904,15 @@ fn candidate_reviewers_from_names<'a>(
863
904
Ok ( valid_candidates)
864
905
}
865
906
}
907
+
908
+ async fn get_previous_reviewer_names ( ctx : & Context , issue : & Issue ) -> HashSet < String > {
909
+ // Get the state of the warnings for this PR in the database.
910
+ let mut db = ctx. db . get ( ) . await ;
911
+ let state: IssueData < ' _ , Reviewer > =
912
+ match IssueData :: load ( & mut db, & issue, PREVIOUS_REVIEWER_KEY ) . await {
913
+ Ok ( state) => state,
914
+ Err ( _) => return HashSet :: new ( ) ,
915
+ } ;
916
+
917
+ state. data . names
918
+ }
0 commit comments