Skip to content

Commit e3d3107

Browse files
committed
Determine whether reviewers were requested directly or not
This will be useful for using a different assignment logic for direct `r?` requests and automatic/team/group rotation assignments.
1 parent 4a9b71f commit e3d3107

File tree

1 file changed

+74
-23
lines changed

1 file changed

+74
-23
lines changed

src/handlers/assign.rs

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -799,44 +799,74 @@ async fn find_reviewer_from_names(
799799
.to_string())
800800
}
801801

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+
802816
/// Recursively expand all teams and adhoc groups found within `names`.
803817
/// Returns a set of expanded usernames.
804818
/// 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.
808819
fn expand_teams_and_groups(
809820
teams: &Teams,
810821
issue: &Issue,
811822
config: &AssignConfig,
812823
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();
816826

817827
// Keep track of groups seen to avoid cycles and avoid expanding the same
818828
// team multiple times.
819829
let mut seen_names = HashSet::new();
820830

831+
enum Candidate<'a> {
832+
Direct(&'a str),
833+
Expanded(&'a str),
834+
}
835+
821836
// This is a queue of potential groups or usernames to expand. The loop
822837
// 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();
825848

826849
// 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+
828856
// `name_to_expand` could be a team name, an adhoc group name or a username.
829857
let maybe_team = get_team_name(teams, issue, name_to_expand);
830858
let maybe_group = strip_organization_prefix(issue, name_to_expand);
831859
let maybe_user = name_to_expand.strip_prefix('@').unwrap_or(name_to_expand);
832860

833861
// Try ad-hoc groups first.
834862
if let Some(group_members) = config.adhoc_groups.get(maybe_group) {
835-
expansion_happened = true;
836-
837863
// If a group has already been expanded, don't expand it again.
838864
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+
);
840870
}
841871
continue;
842872
}
@@ -848,8 +878,7 @@ fn expand_teams_and_groups(
848878
//
849879
// This ignores subteam relationships (it only uses direct members).
850880
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()));
853882
continue;
854883
}
855884

@@ -860,14 +889,31 @@ fn expand_teams_and_groups(
860889
}
861890

862891
// 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+
}
864898
}
865899

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())
867913
}
868914

869915
/// 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.
871917
async fn candidate_reviewers_from_names<'a>(
872918
db: &DbClient,
873919
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
@@ -877,15 +923,23 @@ async fn candidate_reviewers_from_names<'a>(
877923
names: &'a [String],
878924
) -> Result<HashSet<String>, FindReviewerError> {
879925
// 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)?;
881927
let expanded_count = expanded.len();
882928

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+
883936
// Set of candidate usernames to choose from.
884937
// We go through each expanded candidate and store either success or an error for them.
885938
let mut candidates: Vec<Result<String, FindReviewerError>> = Vec::new();
886939

887940
// 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;
889943
let name_lower = candidate.to_lowercase();
890944
let is_pr_author = name_lower == issue.user.login.to_lowercase();
891945
let is_on_vacation = config.is_on_vacation(&candidate);
@@ -916,7 +970,7 @@ async fn candidate_reviewers_from_names<'a>(
916970
if let Some(error_reason) = reason {
917971
candidates.push(Err(error_reason));
918972
} else {
919-
candidates.push(Ok(candidate));
973+
candidates.push(Ok(reviewer_candidate.name));
920974
}
921975
}
922976
assert_eq!(candidates.len(), expanded_count);
@@ -968,9 +1022,6 @@ async fn candidate_reviewers_from_names<'a>(
9681022
.collect();
9691023

9701024
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-
9741025
// If we requested a single user for a review, we return a concrete error message
9751026
// describing why they couldn't be assigned.
9761027
if is_single_user {

0 commit comments

Comments
 (0)