Skip to content

Commit 5692b37

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 5692b37

File tree

3 files changed

+81
-15
lines changed

3 files changed

+81
-15
lines changed

src/handlers/assign.rs

Lines changed: 74 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,24 @@ 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(
268+
ctx: &Context,
269+
issue: &Issue,
270+
github: &GithubClient,
271+
username: &str,
272+
) -> anyhow::Result<()> {
273+
let mut db = ctx.db.get().await;
274+
let mut state: IssueData<'_, Reviewers> =
275+
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await?;
276+
253277
// Don't re-assign if already assigned, e.g. on comment edit
254278
if issue.contain_assignee(&username) {
255279
log::trace!(
256280
"ignoring assign PR {} to {}, already assigned",
257281
issue.global_id(),
258282
username,
259283
);
260-
return;
284+
return Ok(());
261285
}
262286
if let Err(err) = issue.set_assignee(github, &username).await {
263287
log::warn!(
@@ -280,8 +304,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280304
.await
281305
{
282306
log::warn!("failed to post error comment: {e}");
307+
return Err(e);
283308
}
284309
}
310+
311+
// Record the reviewer in the database
312+
state.data.names.insert(username.to_string());
313+
state.save().await?;
314+
Ok(())
285315
}
286316

287317
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +330,12 @@ async fn determine_assignee(
300330
config: &AssignConfig,
301331
diff: &[FileDiff],
302332
) -> anyhow::Result<(Option<String>, bool)> {
303-
let db_client = ctx.db.get().await;
333+
let mut db_client = ctx.db.get().await;
304334
let teams = crate::team_data::teams(&ctx.github).await?;
305335
if let Some(name) = assign_command {
306336
// User included `r?` in the opening PR body.
307337
match find_reviewer_from_names(
308-
&db_client,
338+
&mut db_client,
309339
ctx.workqueue.clone(),
310340
&teams,
311341
config,
@@ -328,7 +358,7 @@ async fn determine_assignee(
328358
match find_reviewers_from_diff(config, diff) {
329359
Ok(candidates) if !candidates.is_empty() => {
330360
match find_reviewer_from_names(
331-
&db_client,
361+
&mut db_client,
332362
ctx.workqueue.clone(),
333363
&teams,
334364
config,
@@ -347,6 +377,7 @@ async fn determine_assignee(
347377
e @ FindReviewerError::NoReviewer { .. }
348378
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
349379
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
380+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
350381
| e @ FindReviewerError::ReviewerOffRotation { .. }
351382
| e @ FindReviewerError::DatabaseError(_)
352383
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
@@ -368,7 +399,7 @@ async fn determine_assignee(
368399

369400
if let Some(fallback) = config.adhoc_groups.get("fallback") {
370401
match find_reviewer_from_names(
371-
&db_client,
402+
&mut db_client,
372403
ctx.workqueue.clone(),
373404
&teams,
374405
config,
@@ -550,10 +581,9 @@ pub(super) async fn handle_command(
550581
issue.remove_assignees(&ctx.github, Selection::All).await?;
551582
return Ok(());
552583
}
553-
554-
let db_client = ctx.db.get().await;
584+
let mut db_client = ctx.db.get().await;
555585
let assignee = match find_reviewer_from_names(
556-
&db_client,
586+
&mut db_client,
557587
ctx.workqueue.clone(),
558588
&teams,
559589
config,
@@ -569,7 +599,7 @@ pub(super) async fn handle_command(
569599
}
570600
};
571601

572-
set_assignee(issue, &ctx.github, &assignee).await;
602+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
573603
} else {
574604
let e = EditIssueBody::new(&issue, "ASSIGN");
575605

@@ -680,6 +710,8 @@ enum FindReviewerError {
680710
ReviewerIsPrAuthor { username: String },
681711
/// Requested reviewer is already assigned to that PR
682712
ReviewerAlreadyAssigned { username: String },
713+
/// Requested reviewer is already assigned previously to that PR.
714+
ReviewerPreviouslyAssigned { username: String },
683715
/// Data required for assignment could not be loaded from the DB.
684716
DatabaseError(String),
685717
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +758,13 @@ impl fmt::Display for FindReviewerError {
726758
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
727759
)
728760
}
761+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
762+
write!(
763+
f,
764+
"{}",
765+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
766+
)
767+
}
729768
FindReviewerError::DatabaseError(error) => {
730769
write!(f, "Database error: {error}")
731770
}
@@ -748,7 +787,7 @@ Please select a different reviewer.",
748787
/// auto-assign groups, or rust-lang team names. It must have at least one
749788
/// entry.
750789
async fn find_reviewer_from_names(
751-
db: &DbClient,
790+
db: &mut DbClient,
752791
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
753792
teams: &Teams,
754793
config: &AssignConfig,
@@ -916,7 +955,7 @@ fn expand_teams_and_groups(
916955
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917956
/// If no reviewer is available, returns an error.
918957
async fn candidate_reviewers_from_names<'a>(
919-
db: &DbClient,
958+
db: &mut DbClient,
920959
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
921960
teams: &'a Teams,
922961
config: &'a AssignConfig,
@@ -925,6 +964,9 @@ async fn candidate_reviewers_from_names<'a>(
925964
) -> Result<HashSet<String>, FindReviewerError> {
926965
// Step 1: expand teams and groups into candidate names
927966
let expanded = expand_teams_and_groups(teams, issue, config, names)?;
967+
let expansion_happend = expanded
968+
.iter()
969+
.any(|c| c.origin == ReviewerCandidateOrigin::Expanded);
928970
let expanded_count = expanded.len();
929971

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

941984
// Step 2: pre-filter candidates based on checks that we can perform quickly
942985
for reviewer_candidate in expanded {
@@ -949,6 +992,8 @@ async fn candidate_reviewers_from_names<'a>(
949992
.iter()
950993
.any(|assignee| name_lower == assignee.login.to_lowercase());
951994

995+
let is_previously_assigned = previous_reviewer_names.contains(&reviewer_candidate.name);
996+
952997
// Record the reason why the candidate was filtered out
953998
let reason = {
954999
if is_pr_author {
@@ -963,6 +1008,12 @@ async fn candidate_reviewers_from_names<'a>(
9631008
Some(FindReviewerError::ReviewerAlreadyAssigned {
9641009
username: candidate.clone(),
9651010
})
1011+
} else if expansion_happend && is_previously_assigned {
1012+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
1013+
// `r? @reviewer` will not consider the reviewer previously assigned
1014+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
1015+
username: candidate.clone(),
1016+
})
9661017
} else {
9671018
None
9681019
}
@@ -1058,3 +1109,13 @@ async fn candidate_reviewers_from_names<'a>(
10581109
.collect())
10591110
}
10601111
}
1112+
1113+
async fn get_previous_reviewer_names(db: &mut DbClient, issue: &Issue) -> HashSet<String> {
1114+
let state: IssueData<'_, Reviewers> =
1115+
match IssueData::load(db, &issue, PREVIOUS_REVIEWER_KEY).await {
1116+
Ok(state) => state,
1117+
Err(_) => return HashSet::new(),
1118+
};
1119+
1120+
state.data.names
1121+
}

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)