@@ -799,44 +799,74 @@ async fn find_reviewer_from_names(
799
799
. to_string ( ) )
800
800
}
801
801
802
+ #[ derive( Eq , PartialEq , Hash ) ]
803
+ struct ReviewerCandidate {
804
+ name : String ,
805
+ origin : ReviewerCandidateOrigin ,
806
+ }
807
+
808
+ #[ derive( Eq , PartialEq , Hash , Copy , Clone ) ]
809
+ enum ReviewerCandidateOrigin {
810
+ /// This reviewer was directly requested for a review.
811
+ Direct ,
812
+ /// This reviewer was expanded from a team or an assign group.
813
+ Expanded ,
814
+ }
815
+
802
816
/// Recursively expand all teams and adhoc groups found within `names`.
803
817
/// Returns a set of expanded usernames.
804
818
/// Also normalizes usernames from `@user` to `user`.
805
- ///
806
- /// Returns `(set of expanded users, expansion_happened)`.
807
- /// `expansion_happened` signals if any expansion has been performed.
808
819
fn expand_teams_and_groups (
809
820
teams : & Teams ,
810
821
issue : & Issue ,
811
822
config : & AssignConfig ,
812
823
names : & [ String ] ,
813
- ) -> Result < ( HashSet < String > , bool ) , FindReviewerError > {
814
- let mut expanded = HashSet :: new ( ) ;
815
- let mut expansion_happened = false ;
824
+ ) -> Result < HashSet < ReviewerCandidate > , FindReviewerError > {
825
+ let mut selected_candidates: HashSet < String > = HashSet :: new ( ) ;
816
826
817
827
// Keep track of groups seen to avoid cycles and avoid expanding the same
818
828
// team multiple times.
819
829
let mut seen_names = HashSet :: new ( ) ;
820
830
831
+ enum Candidate < ' a > {
832
+ Direct ( & ' a str ) ,
833
+ Expanded ( & ' a str ) ,
834
+ }
835
+
821
836
// This is a queue of potential groups or usernames to expand. The loop
822
837
// below will pop from this and then append the expanded results of teams.
823
- // Usernames will be added to `expanded`.
824
- let mut to_be_expanded: Vec < & str > = names. iter ( ) . map ( |n| n. as_str ( ) ) . collect ( ) ;
838
+ // Usernames will be added to `selected_candidates`.
839
+ let mut to_be_expanded: Vec < Candidate > = names
840
+ . iter ( )
841
+ . map ( |n| Candidate :: Direct ( n. as_str ( ) ) )
842
+ . collect ( ) ;
843
+
844
+ // We store the directly requested usernames (after normalization).
845
+ // A username can be both directly requested and expanded from a group/team, the former
846
+ // should have priority.
847
+ let mut directly_requested: HashSet < & str > = HashSet :: new ( ) ;
825
848
826
849
// Loop over names to recursively expand them.
827
- while let Some ( name_to_expand) = to_be_expanded. pop ( ) {
850
+ while let Some ( candidate) = to_be_expanded. pop ( ) {
851
+ let name_to_expand = match & candidate {
852
+ Candidate :: Direct ( name) => name,
853
+ Candidate :: Expanded ( name) => name,
854
+ } ;
855
+
828
856
// `name_to_expand` could be a team name, an adhoc group name or a username.
829
857
let maybe_team = get_team_name ( teams, issue, name_to_expand) ;
830
858
let maybe_group = strip_organization_prefix ( issue, name_to_expand) ;
831
859
let maybe_user = name_to_expand. strip_prefix ( '@' ) . unwrap_or ( name_to_expand) ;
832
860
833
861
// Try ad-hoc groups first.
834
862
if let Some ( group_members) = config. adhoc_groups . get ( maybe_group) {
835
- expansion_happened = true ;
836
-
837
863
// If a group has already been expanded, don't expand it again.
838
864
if seen_names. insert ( maybe_group) {
839
- to_be_expanded. extend ( group_members. iter ( ) . map ( |s| s. as_str ( ) ) ) ;
865
+ to_be_expanded. extend (
866
+ group_members
867
+ . iter ( )
868
+ . map ( |s| Candidate :: Expanded ( s. as_str ( ) ) ) ,
869
+ ) ;
840
870
}
841
871
continue ;
842
872
}
@@ -848,8 +878,7 @@ fn expand_teams_and_groups(
848
878
//
849
879
// This ignores subteam relationships (it only uses direct members).
850
880
if let Some ( team) = maybe_team. and_then ( |t| teams. teams . get ( t) ) {
851
- expansion_happened = true ;
852
- expanded. extend ( team. members . iter ( ) . map ( |member| member. github . clone ( ) ) ) ;
881
+ selected_candidates. extend ( team. members . iter ( ) . map ( |member| member. github . clone ( ) ) ) ;
853
882
continue ;
854
883
}
855
884
@@ -860,14 +889,31 @@ fn expand_teams_and_groups(
860
889
}
861
890
862
891
// Assume it is a user.
863
- expanded. insert ( maybe_user. to_string ( ) ) ;
892
+ let username = maybe_user. to_string ( ) ;
893
+ selected_candidates. insert ( username) ;
894
+
895
+ if let Candidate :: Direct ( _) = candidate {
896
+ directly_requested. insert ( maybe_user) ;
897
+ }
864
898
}
865
899
866
- Ok ( ( expanded, expansion_happened) )
900
+ // Now that we have a unique set of candidates, figure out which ones of them were requested
901
+ // directly.
902
+ Ok ( selected_candidates
903
+ . into_iter ( )
904
+ . map ( |name| {
905
+ let origin = if directly_requested. contains ( name. as_str ( ) ) {
906
+ ReviewerCandidateOrigin :: Direct
907
+ } else {
908
+ ReviewerCandidateOrigin :: Expanded
909
+ } ;
910
+ ReviewerCandidate { name, origin }
911
+ } )
912
+ . collect ( ) )
867
913
}
868
914
869
915
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
870
- /// If not reviewer is available, returns an error.
916
+ /// If no reviewer is available, returns an error.
871
917
async fn candidate_reviewers_from_names < ' a > (
872
918
db : & DbClient ,
873
919
workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
@@ -877,15 +923,23 @@ async fn candidate_reviewers_from_names<'a>(
877
923
names : & ' a [ String ] ,
878
924
) -> Result < HashSet < String > , FindReviewerError > {
879
925
// Step 1: expand teams and groups into candidate names
880
- let ( expanded, expansion_happened ) = expand_teams_and_groups ( teams, issue, config, names) ?;
926
+ let expanded = expand_teams_and_groups ( teams, issue, config, names) ?;
881
927
let expanded_count = expanded. len ( ) ;
882
928
929
+ // Was it a request for a single user, i.e. `r? @username`?
930
+ let is_single_user = expanded_count == 1
931
+ && matches ! (
932
+ expanded. iter( ) . next( ) . map( |c| c. origin) ,
933
+ Some ( ReviewerCandidateOrigin :: Direct )
934
+ ) ;
935
+
883
936
// Set of candidate usernames to choose from.
884
937
// We go through each expanded candidate and store either success or an error for them.
885
938
let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
886
939
887
940
// Step 2: pre-filter candidates based on checks that we can perform quickly
888
- for candidate in expanded {
941
+ for reviewer_candidate in expanded {
942
+ let candidate = & reviewer_candidate. name ;
889
943
let name_lower = candidate. to_lowercase ( ) ;
890
944
let is_pr_author = name_lower == issue. user . login . to_lowercase ( ) ;
891
945
let is_on_vacation = config. is_on_vacation ( & candidate) ;
@@ -916,7 +970,7 @@ async fn candidate_reviewers_from_names<'a>(
916
970
if let Some ( error_reason) = reason {
917
971
candidates. push ( Err ( error_reason) ) ;
918
972
} else {
919
- candidates. push ( Ok ( candidate ) ) ;
973
+ candidates. push ( Ok ( reviewer_candidate . name ) ) ;
920
974
}
921
975
}
922
976
assert_eq ! ( candidates. len( ) , expanded_count) ;
@@ -968,9 +1022,6 @@ async fn candidate_reviewers_from_names<'a>(
968
1022
. collect ( ) ;
969
1023
970
1024
if valid_candidates. is_empty ( ) {
971
- // Was it a request for a single user, i.e. `r? @username`?
972
- let is_single_user = names. len ( ) == 1 && !expansion_happened;
973
-
974
1025
// If we requested a single user for a review, we return a concrete error message
975
1026
// describing why they couldn't be assigned.
976
1027
if is_single_user {
0 commit comments