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;
24
+ use crate :: github:: UserId ;
23
25
use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
24
26
use crate :: {
25
27
config:: AssignConfig ,
@@ -345,7 +347,9 @@ async fn determine_assignee(
345
347
e @ FindReviewerError :: NoReviewer { .. }
346
348
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
347
349
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
348
- | e @ FindReviewerError :: ReviewerOnVacation { .. } ,
350
+ | e @ FindReviewerError :: ReviewerOnVacation { .. }
351
+ | e @ FindReviewerError :: DatabaseError ( _)
352
+ | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
349
353
) => log:: trace!(
350
354
"no reviewer could be determined for PR {}: {e}" ,
351
355
event. issue. global_id( )
@@ -675,6 +679,10 @@ enum FindReviewerError {
675
679
ReviewerIsPrAuthor { username : String } ,
676
680
/// Requested reviewer is already assigned to that PR
677
681
ReviewerAlreadyAssigned { username : String } ,
682
+ /// Data required for assignment could not be loaded from the DB.
683
+ DatabaseError ( String ) ,
684
+ /// The reviewer has too many PRs alreayd assigned.
685
+ ReviewerAtMaxCapacity { username : String } ,
678
686
}
679
687
680
688
impl std:: error:: Error for FindReviewerError { }
@@ -717,6 +725,17 @@ impl fmt::Display for FindReviewerError {
717
725
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
718
726
)
719
727
}
728
+ FindReviewerError :: DatabaseError ( error) => {
729
+ write ! ( f, "Database error: {error}" )
730
+ }
731
+ FindReviewerError :: ReviewerAtMaxCapacity { username } => {
732
+ write ! (
733
+ f,
734
+ r"`{username}` has too many PRs assigned to them.
735
+
736
+ Please select a different reviewer." ,
737
+ )
738
+ }
720
739
}
721
740
}
722
741
}
@@ -728,7 +747,7 @@ impl fmt::Display for FindReviewerError {
728
747
/// auto-assign groups, or rust-lang team names. It must have at least one
729
748
/// entry.
730
749
async fn find_reviewer_from_names (
731
- _db : & DbClient ,
750
+ db : & DbClient ,
732
751
workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
733
752
teams : & Teams ,
734
753
config : & AssignConfig ,
@@ -742,7 +761,10 @@ async fn find_reviewer_from_names(
742
761
}
743
762
}
744
763
745
- let candidates = candidate_reviewers_from_names ( workqueue, teams, config, issue, names) ?;
764
+ let candidates =
765
+ candidate_reviewers_from_names ( db, workqueue, teams, config, issue, names) . await ?;
766
+ assert ! ( !candidates. is_empty( ) ) ;
767
+
746
768
// This uses a relatively primitive random choice algorithm.
747
769
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
748
770
//
@@ -846,20 +868,23 @@ fn expand_teams_and_groups(
846
868
847
869
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
848
870
/// If not reviewer is available, returns an error.
849
- fn candidate_reviewers_from_names < ' a > (
871
+ async fn candidate_reviewers_from_names < ' a > (
872
+ db : & DbClient ,
850
873
workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
851
874
teams : & ' a Teams ,
852
875
config : & ' a AssignConfig ,
853
876
issue : & Issue ,
854
877
names : & ' a [ String ] ,
855
878
) -> Result < HashSet < String > , FindReviewerError > {
879
+ // Step 1: expand teams and groups into candidate names
856
880
let ( expanded, expansion_happened) = expand_teams_and_groups ( teams, issue, config, names) ?;
857
881
let expanded_count = expanded. len ( ) ;
858
882
859
883
// Set of candidate usernames to choose from.
860
884
// We go through each expanded candidate and store either success or an error for them.
861
885
let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
862
886
887
+ // Step 2: pre-filter candidates based on checks that we can perform quickly
863
888
for candidate in expanded {
864
889
let name_lower = candidate. to_lowercase ( ) ;
865
890
let is_pr_author = name_lower == issue. user . login . to_lowercase ( ) ;
@@ -896,9 +921,50 @@ fn candidate_reviewers_from_names<'a>(
896
921
}
897
922
assert_eq ! ( candidates. len( ) , expanded_count) ;
898
923
899
- let valid_candidates: HashSet < String > = candidates
924
+ if config. review_prefs . is_some ( ) {
925
+ // Step 3: gather potential usernames to form a DB query for review preferences
926
+ let usernames: Vec < String > = candidates
927
+ . iter ( )
928
+ . filter_map ( |res| res. as_deref ( ) . ok ( ) . map ( |s| s. to_string ( ) ) )
929
+ . collect ( ) ;
930
+ let usernames: Vec < & str > = usernames. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
931
+ let review_prefs = get_review_prefs_batch ( db, & usernames)
932
+ . await
933
+ . context ( "cannot fetch review preferences" )
934
+ . map_err ( |e| FindReviewerError :: DatabaseError ( e. to_string ( ) ) ) ?;
935
+
936
+ let workqueue = workqueue. read ( ) . await ;
937
+
938
+ // Step 4: check review preferences
939
+ candidates = candidates
940
+ . into_iter ( )
941
+ . map ( |username| {
942
+ // Only consider candidates that did not have an earlier error
943
+ let username = username?;
944
+
945
+ // If no review prefs were found, we assume the default unlimited
946
+ // review capacity.
947
+ let Some ( review_prefs) = review_prefs. get ( username. as_str ( ) ) else {
948
+ return Ok ( username) ;
949
+ } ;
950
+ let Some ( capacity) = review_prefs. max_assigned_prs else {
951
+ return Ok ( username) ;
952
+ } ;
953
+ let assigned_prs = workqueue. assigned_pr_count ( review_prefs. user_id as UserId ) ;
954
+ // Can we assign one more PR?
955
+ if ( assigned_prs as i32 ) < capacity {
956
+ Ok ( username)
957
+ } else {
958
+ Err ( FindReviewerError :: ReviewerAtMaxCapacity { username } )
959
+ }
960
+ } )
961
+ . collect ( ) ;
962
+ }
963
+ assert_eq ! ( candidates. len( ) , expanded_count) ;
964
+
965
+ let valid_candidates: HashSet < & str > = candidates
900
966
. iter ( )
901
- . filter_map ( |res| res. as_ref ( ) . ok ( ) . cloned ( ) )
967
+ . filter_map ( |res| res. as_deref ( ) . ok ( ) )
902
968
. collect ( ) ;
903
969
904
970
if valid_candidates. is_empty ( ) {
@@ -925,6 +991,9 @@ fn candidate_reviewers_from_names<'a>(
925
991
} )
926
992
}
927
993
} else {
928
- Ok ( valid_candidates)
994
+ Ok ( valid_candidates
995
+ . into_iter ( )
996
+ . map ( |s| s. to_string ( ) )
997
+ . collect ( ) )
929
998
}
930
999
}
0 commit comments