Skip to content

Commit 1c72dfc

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 c335263 commit 1c72dfc

File tree

3 files changed

+79
-15
lines changed

3 files changed

+79
-15
lines changed

src/handlers/assign.rs

Lines changed: 72 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,22 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9293
9394
Please choose another assignee.";
9495

96+
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewer @{username} was already 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_REVIEWERS_KEY: &str = "previous-reviewers";
105+
106+
/// State stored in the database
107+
#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize, serde::Serialize)]
108+
struct Reviewers {
109+
names: HashSet<String>,
110+
}
111+
98112
/// Assignment data stored in the issue/PR body.
99113
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
100114
struct AssignData {
@@ -217,7 +231,7 @@ pub(super) async fn handle_input(
217231
None
218232
};
219233
if let Some(assignee) = assignee {
220-
set_assignee(&event.issue, &ctx.github, &assignee).await;
234+
set_assignee(&ctx, &event.issue, &ctx.github, &assignee).await?;
221235
}
222236

223237
if let Some(welcome) = welcome {
@@ -249,15 +263,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249263
}
250264

251265
/// Sets the assignee of a PR, alerting any errors.
252-
async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
266+
async fn set_assignee(
267+
ctx: &Context,
268+
issue: &Issue,
269+
github: &GithubClient,
270+
username: &str,
271+
) -> anyhow::Result<()> {
272+
let mut db = ctx.db.get().await;
273+
let mut state: IssueData<'_, Reviewers> =
274+
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWERS_KEY).await?;
275+
253276
// Don't re-assign if already assigned, e.g. on comment edit
254277
if issue.contain_assignee(&username) {
255278
log::trace!(
256279
"ignoring assign PR {} to {}, already assigned",
257280
issue.global_id(),
258281
username,
259282
);
260-
return;
283+
return Ok(());
261284
}
262285
if let Err(err) = issue.set_assignee(github, &username).await {
263286
log::warn!(
@@ -280,8 +303,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280303
.await
281304
{
282305
log::warn!("failed to post error comment: {e}");
306+
return Err(e);
283307
}
284308
}
309+
310+
// Record the reviewer in the database
311+
state.data.names.insert(username.to_string());
312+
state.save().await?;
313+
Ok(())
285314
}
286315

287316
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +329,12 @@ async fn determine_assignee(
300329
config: &AssignConfig,
301330
diff: &[FileDiff],
302331
) -> anyhow::Result<(Option<String>, bool)> {
303-
let db_client = ctx.db.get().await;
332+
let mut db_client = ctx.db.get().await;
304333
let teams = crate::team_data::teams(&ctx.github).await?;
305334
if let Some(name) = assign_command {
306335
// User included `r?` in the opening PR body.
307336
match find_reviewer_from_names(
308-
&db_client,
337+
&mut db_client,
309338
ctx.workqueue.clone(),
310339
&teams,
311340
config,
@@ -328,7 +357,7 @@ async fn determine_assignee(
328357
match find_reviewers_from_diff(config, diff) {
329358
Ok(candidates) if !candidates.is_empty() => {
330359
match find_reviewer_from_names(
331-
&db_client,
360+
&mut db_client,
332361
ctx.workqueue.clone(),
333362
&teams,
334363
config,
@@ -347,6 +376,7 @@ async fn determine_assignee(
347376
e @ FindReviewerError::NoReviewer { .. }
348377
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
349378
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
379+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
350380
| e @ FindReviewerError::ReviewerOffRotation { .. }
351381
| e @ FindReviewerError::DatabaseError(_)
352382
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
@@ -368,7 +398,7 @@ async fn determine_assignee(
368398

369399
if let Some(fallback) = config.adhoc_groups.get("fallback") {
370400
match find_reviewer_from_names(
371-
&db_client,
401+
&mut db_client,
372402
ctx.workqueue.clone(),
373403
&teams,
374404
config,
@@ -550,10 +580,9 @@ pub(super) async fn handle_command(
550580
issue.remove_assignees(&ctx.github, Selection::All).await?;
551581
return Ok(());
552582
}
553-
554-
let db_client = ctx.db.get().await;
583+
let mut db_client = ctx.db.get().await;
555584
let assignee = match find_reviewer_from_names(
556-
&db_client,
585+
&mut db_client,
557586
ctx.workqueue.clone(),
558587
&teams,
559588
config,
@@ -569,7 +598,7 @@ pub(super) async fn handle_command(
569598
}
570599
};
571600

572-
set_assignee(issue, &ctx.github, &assignee).await;
601+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
573602
} else {
574603
let e = EditIssueBody::new(&issue, "ASSIGN");
575604

@@ -680,6 +709,8 @@ enum FindReviewerError {
680709
ReviewerIsPrAuthor { username: String },
681710
/// Requested reviewer is already assigned to that PR
682711
ReviewerAlreadyAssigned { username: String },
712+
/// Requested reviewer was already assigned previously to that PR.
713+
ReviewerPreviouslyAssigned { username: String },
683714
/// Data required for assignment could not be loaded from the DB.
684715
DatabaseError(String),
685716
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +757,13 @@ impl fmt::Display for FindReviewerError {
726757
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
727758
)
728759
}
760+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
761+
write!(
762+
f,
763+
"{}",
764+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
765+
)
766+
}
729767
FindReviewerError::DatabaseError(error) => {
730768
write!(f, "Database error: {error}")
731769
}
@@ -748,7 +786,7 @@ Please select a different reviewer.",
748786
/// auto-assign groups, or rust-lang team names. It must have at least one
749787
/// entry.
750788
async fn find_reviewer_from_names(
751-
db: &DbClient,
789+
db: &mut DbClient,
752790
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
753791
teams: &Teams,
754792
config: &AssignConfig,
@@ -916,7 +954,7 @@ fn expand_teams_and_groups(
916954
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917955
/// If no reviewer is available, returns an error.
918956
async fn candidate_reviewers_from_names<'a>(
919-
db: &DbClient,
957+
db: &mut DbClient,
920958
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
921959
teams: &'a Teams,
922960
config: &'a AssignConfig,
@@ -937,6 +975,7 @@ async fn candidate_reviewers_from_names<'a>(
937975
// Set of candidate usernames to choose from.
938976
// We go through each expanded candidate and store either success or an error for them.
939977
let mut candidates: Vec<Result<String, FindReviewerError>> = Vec::new();
978+
let previous_reviewer_names = get_previous_reviewer_names(db, issue).await;
940979

941980
// Step 2: pre-filter candidates based on checks that we can perform quickly
942981
for reviewer_candidate in expanded {
@@ -949,6 +988,8 @@ async fn candidate_reviewers_from_names<'a>(
949988
.iter()
950989
.any(|assignee| name_lower == assignee.login.to_lowercase());
951990

991+
let is_previously_assigned = previous_reviewer_names.contains(&reviewer_candidate.name);
992+
952993
// Record the reason why the candidate was filtered out
953994
let reason = {
954995
if is_pr_author {
@@ -963,6 +1004,14 @@ async fn candidate_reviewers_from_names<'a>(
9631004
Some(FindReviewerError::ReviewerAlreadyAssigned {
9641005
username: candidate.clone(),
9651006
})
1007+
} else if reviewer_candidate.origin == ReviewerCandidateOrigin::Expanded
1008+
&& is_previously_assigned
1009+
{
1010+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
1011+
// `r? @reviewer` will not consider the reviewer previously assigned
1012+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
1013+
username: candidate.clone(),
1014+
})
9661015
} else {
9671016
None
9681017
}
@@ -1058,3 +1107,13 @@ async fn candidate_reviewers_from_names<'a>(
10581107
.collect())
10591108
}
10601109
}
1110+
1111+
async fn get_previous_reviewer_names(db: &mut DbClient, issue: &Issue) -> HashSet<String> {
1112+
let state: IssueData<'_, Reviewers> =
1113+
match IssueData::load(db, &issue, PREVIOUS_REVIEWERS_KEY).await {
1114+
Ok(state) => state,
1115+
Err(_) => return HashSet::new(),
1116+
};
1117+
1118+
state.data.names
1119+
}

src/handlers/assign/tests/tests_candidates.rs

Lines changed: 3 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

@@ -72,14 +73,14 @@ impl AssignCtx {
7273
}
7374

7475
async fn check(
75-
self,
76+
mut self,
7677
names: &[&str],
7778
expected: Result<&[&str], FindReviewerError>,
7879
) -> anyhow::Result<TestContext> {
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+
self.test_ctx.db_client_mut(),
8384
Arc::new(RwLock::new(self.reviewer_workqueue)),
8485
&self.teams,
8586
&self.config,

src/tests/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ impl TestContext {
9494
&self.client
9595
}
9696

97+
pub(crate) fn db_client_mut(&mut self) -> &mut PooledClient {
98+
&mut self.client
99+
}
100+
97101
pub(crate) async fn add_user(&self, name: &str, id: u64) {
98102
record_username(self.db_client(), id, name)
99103
.await

0 commit comments

Comments
 (0)