Skip to content

Commit b0574b6

Browse files
committed
Pick reviewer who is not previous assignee when r? group
This commit optimized the logic for `r? group`. Each time an assignee is set, it is stored in the database, and when we use an r? group, such as `r? compiler`, it will take out the previously assigned reviewers from the database to avoid to assign a previous assignee. Signed-off-by: xizheyin <[email protected]>
1 parent 1060398 commit b0574b6

File tree

2 files changed

+69
-15
lines changed

2 files changed

+69
-15
lines changed

src/handlers/assign.rs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
//! `assign.owners` config, it will auto-select an assignee based on the files
2121
//! the PR modifies.
2222
23+
use crate::db::issue_data::IssueData;
2324
use crate::db::review_prefs::{get_review_prefs_batch, RotationMode};
2425
use crate::github::UserId;
2526
use crate::handlers::pr_tracking::ReviewerWorkqueue;
@@ -92,9 +93,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9293
9394
Please choose another assignee.";
9495

96+
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewers are assigned before.
97+
98+
Please choose another assignee by using `r? @reviewer`.";
99+
95100
// Special account that we use to prevent assignment.
96101
const GHOST_ACCOUNT: &str = "ghost";
97102

103+
/// Key for the state in the database
104+
const PREVIOUS_REVIEWER_KEY: &str = "previous-reviewer";
105+
106+
/// State stored in the database
107+
#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize, serde::Serialize)]
108+
struct Reviewers {
109+
/// List of the last warnings in the most recent comment.
110+
names: HashSet<String>,
111+
}
112+
98113
/// Assignment data stored in the issue/PR body.
99114
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
100115
struct AssignData {
@@ -217,7 +232,7 @@ pub(super) async fn handle_input(
217232
None
218233
};
219234
if let Some(assignee) = assignee {
220-
set_assignee(&event.issue, &ctx.github, &assignee).await;
235+
set_assignee(&ctx, &event.issue, &ctx.github, &assignee).await?;
221236
}
222237

223238
if let Some(welcome) = welcome {
@@ -249,15 +264,19 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249264
}
250265

251266
/// Sets the assignee of a PR, alerting any errors.
252-
async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
267+
async fn set_assignee(ctx: &Context, issue: &Issue, github: &GithubClient, username: &str)-> anyhow::Result<()>{
268+
let mut db = ctx.db.get().await;
269+
let mut state: IssueData<'_, Reviewers> =
270+
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await?;
271+
253272
// Don't re-assign if already assigned, e.g. on comment edit
254273
if issue.contain_assignee(&username) {
255274
log::trace!(
256275
"ignoring assign PR {} to {}, already assigned",
257276
issue.global_id(),
258277
username,
259278
);
260-
return;
279+
return Ok(());
261280
}
262281
if let Err(err) = issue.set_assignee(github, &username).await {
263282
log::warn!(
@@ -280,8 +299,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280299
.await
281300
{
282301
log::warn!("failed to post error comment: {e}");
302+
return Err(e);
283303
}
284304
}
305+
306+
// Record the reviewer in the database
307+
state.data.names.insert(username.to_string());
308+
state.save().await?;
309+
Ok(())
285310
}
286311

287312
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +325,12 @@ async fn determine_assignee(
300325
config: &AssignConfig,
301326
diff: &[FileDiff],
302327
) -> anyhow::Result<(Option<String>, bool)> {
303-
let db_client = ctx.db.get().await;
328+
let mut db_client = ctx.db.get().await;
304329
let teams = crate::team_data::teams(&ctx.github).await?;
305330
if let Some(name) = assign_command {
306331
// User included `r?` in the opening PR body.
307332
match find_reviewer_from_names(
308-
&db_client,
333+
&mut db_client,
309334
ctx.workqueue.clone(),
310335
&teams,
311336
config,
@@ -328,7 +353,7 @@ async fn determine_assignee(
328353
match find_reviewers_from_diff(config, diff) {
329354
Ok(candidates) if !candidates.is_empty() => {
330355
match find_reviewer_from_names(
331-
&db_client,
356+
&mut db_client,
332357
ctx.workqueue.clone(),
333358
&teams,
334359
config,
@@ -347,6 +372,7 @@ async fn determine_assignee(
347372
e @ FindReviewerError::NoReviewer { .. }
348373
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
349374
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
375+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
350376
| e @ FindReviewerError::ReviewerOffRotation { .. }
351377
| e @ FindReviewerError::DatabaseError(_)
352378
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
@@ -368,7 +394,7 @@ async fn determine_assignee(
368394

369395
if let Some(fallback) = config.adhoc_groups.get("fallback") {
370396
match find_reviewer_from_names(
371-
&db_client,
397+
&mut db_client,
372398
ctx.workqueue.clone(),
373399
&teams,
374400
config,
@@ -550,10 +576,9 @@ pub(super) async fn handle_command(
550576
issue.remove_assignees(&ctx.github, Selection::All).await?;
551577
return Ok(());
552578
}
553-
554-
let db_client = ctx.db.get().await;
579+
let mut db_client = ctx.db.get().await;
555580
let assignee = match find_reviewer_from_names(
556-
&db_client,
581+
&mut db_client,
557582
ctx.workqueue.clone(),
558583
&teams,
559584
config,
@@ -569,7 +594,7 @@ pub(super) async fn handle_command(
569594
}
570595
};
571596

572-
set_assignee(issue, &ctx.github, &assignee).await;
597+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
573598
} else {
574599
let e = EditIssueBody::new(&issue, "ASSIGN");
575600

@@ -680,6 +705,8 @@ enum FindReviewerError {
680705
ReviewerIsPrAuthor { username: String },
681706
/// Requested reviewer is already assigned to that PR
682707
ReviewerAlreadyAssigned { username: String },
708+
/// Requested reviewer is already assigned previously to that PR.
709+
ReviewerPreviouslyAssigned { username: String },
683710
/// Data required for assignment could not be loaded from the DB.
684711
DatabaseError(String),
685712
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +753,13 @@ impl fmt::Display for FindReviewerError {
726753
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
727754
)
728755
}
756+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
757+
write!(
758+
f,
759+
"{}",
760+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
761+
)
762+
}
729763
FindReviewerError::DatabaseError(error) => {
730764
write!(f, "Database error: {error}")
731765
}
@@ -748,7 +782,7 @@ Please select a different reviewer.",
748782
/// auto-assign groups, or rust-lang team names. It must have at least one
749783
/// entry.
750784
async fn find_reviewer_from_names(
751-
db: &DbClient,
785+
db: &mut DbClient,
752786
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
753787
teams: &Teams,
754788
config: &AssignConfig,
@@ -916,7 +950,7 @@ fn expand_teams_and_groups(
916950
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917951
/// If no reviewer is available, returns an error.
918952
async fn candidate_reviewers_from_names<'a>(
919-
db: &DbClient,
953+
db: &mut DbClient,
920954
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
921955
teams: &'a Teams,
922956
config: &'a AssignConfig,
@@ -925,6 +959,7 @@ async fn candidate_reviewers_from_names<'a>(
925959
) -> Result<HashSet<String>, FindReviewerError> {
926960
// Step 1: expand teams and groups into candidate names
927961
let expanded = expand_teams_and_groups(teams, issue, config, names)?;
962+
let expansion_happend = expanded.iter().any(|c| c.origin == ReviewerCandidateOrigin::Expanded);
928963
let expanded_count = expanded.len();
929964

930965
// Was it a request for a single user, i.e. `r? @username`?
@@ -937,6 +972,7 @@ async fn candidate_reviewers_from_names<'a>(
937972
// Set of candidate usernames to choose from.
938973
// We go through each expanded candidate and store either success or an error for them.
939974
let mut candidates: Vec<Result<String, FindReviewerError>> = Vec::new();
975+
let previous_reviewer_names = get_previous_reviewer_names(db, issue).await;
940976

941977
// Step 2: pre-filter candidates based on checks that we can perform quickly
942978
for reviewer_candidate in expanded {
@@ -949,6 +985,8 @@ async fn candidate_reviewers_from_names<'a>(
949985
.iter()
950986
.any(|assignee| name_lower == assignee.login.to_lowercase());
951987

988+
let is_previously_assigned = previous_reviewer_names.contains(&reviewer_candidate.name);
989+
952990
// Record the reason why the candidate was filtered out
953991
let reason = {
954992
if is_pr_author {
@@ -963,6 +1001,12 @@ async fn candidate_reviewers_from_names<'a>(
9631001
Some(FindReviewerError::ReviewerAlreadyAssigned {
9641002
username: candidate.clone(),
9651003
})
1004+
} else if expansion_happend && is_previously_assigned {
1005+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
1006+
// `r? @reviewer` will not consider the reviewer previously assigned
1007+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
1008+
username: candidate.clone(),
1009+
})
9661010
} else {
9671011
None
9681012
}
@@ -1058,3 +1102,13 @@ async fn candidate_reviewers_from_names<'a>(
10581102
.collect())
10591103
}
10601104
}
1105+
1106+
async fn get_previous_reviewer_names(db: &mut DbClient, issue: &Issue) -> HashSet<String> {
1107+
let state: IssueData<'_, Reviewers> =
1108+
match IssueData::load(db, &issue, PREVIOUS_REVIEWER_KEY).await {
1109+
Ok(state) => state,
1110+
Err(_) => return HashSet::new(),
1111+
};
1112+
1113+
state.data.names
1114+
}

src/handlers/assign/tests/tests_candidates.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::super::*;
44
use crate::db::review_prefs::{upsert_review_prefs, RotationMode};
55
use crate::github::{PullRequestNumber, User};
6+
use crate::handlers::pr_tracking::ReviewerWorkqueue;
67
use crate::tests::github::{issue, user};
78
use crate::tests::{run_db_test, TestContext};
89

@@ -79,8 +80,7 @@ impl AssignCtx {
7980
let names: Vec<_> = names.iter().map(|n| n.to_string()).collect();
8081

8182
let reviewers = candidate_reviewers_from_names(
82-
self.test_ctx.db_client(),
83-
Arc::new(RwLock::new(self.reviewer_workqueue)),
83+
self.test_ctx.handler_ctx(),
8484
&self.teams,
8585
&self.config,
8686
&self.issue,

0 commit comments

Comments
 (0)