Skip to content

Commit 874e154

Browse files
authored
Merge pull request #1958 from xizheyin/reroll
Pick reviewer who is not previous assignee when r? group
2 parents 1bdbf5c + f56e596 commit 874e154

File tree

3 files changed

+149
-15
lines changed

3 files changed

+149
-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 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 are stored in lowercase
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_REVIEWERS_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,15 @@ 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+
313+
state.data.names.insert(username.to_lowercase());
314+
state.save().await?;
315+
Ok(())
285316
}
286317

287318
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,12 +331,12 @@ async fn determine_assignee(
300331
config: &AssignConfig,
301332
diff: &[FileDiff],
302333
) -> anyhow::Result<(Option<String>, bool)> {
303-
let db_client = ctx.db.get().await;
334+
let mut db_client = ctx.db.get().await;
304335
let teams = crate::team_data::teams(&ctx.github).await?;
305336
if let Some(name) = assign_command {
306337
// User included `r?` in the opening PR body.
307338
match find_reviewer_from_names(
308-
&db_client,
339+
&mut db_client,
309340
ctx.workqueue.clone(),
310341
&teams,
311342
config,
@@ -328,7 +359,7 @@ async fn determine_assignee(
328359
match find_reviewers_from_diff(config, diff) {
329360
Ok(candidates) if !candidates.is_empty() => {
330361
match find_reviewer_from_names(
331-
&db_client,
362+
&mut db_client,
332363
ctx.workqueue.clone(),
333364
&teams,
334365
config,
@@ -347,6 +378,7 @@ async fn determine_assignee(
347378
e @ FindReviewerError::NoReviewer { .. }
348379
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
349380
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
381+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
350382
| e @ FindReviewerError::ReviewerOffRotation { .. }
351383
| e @ FindReviewerError::DatabaseError(_)
352384
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
@@ -368,7 +400,7 @@ async fn determine_assignee(
368400

369401
if let Some(fallback) = config.adhoc_groups.get("fallback") {
370402
match find_reviewer_from_names(
371-
&db_client,
403+
&mut db_client,
372404
ctx.workqueue.clone(),
373405
&teams,
374406
config,
@@ -550,10 +582,9 @@ pub(super) async fn handle_command(
550582
issue.remove_assignees(&ctx.github, Selection::All).await?;
551583
return Ok(());
552584
}
553-
554-
let db_client = ctx.db.get().await;
585+
let mut db_client = ctx.db.get().await;
555586
let assignee = match find_reviewer_from_names(
556-
&db_client,
587+
&mut db_client,
557588
ctx.workqueue.clone(),
558589
&teams,
559590
config,
@@ -569,7 +600,7 @@ pub(super) async fn handle_command(
569600
}
570601
};
571602

572-
set_assignee(issue, &ctx.github, &assignee).await;
603+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
573604
} else {
574605
let e = EditIssueBody::new(&issue, "ASSIGN");
575606

@@ -680,6 +711,8 @@ enum FindReviewerError {
680711
ReviewerIsPrAuthor { username: String },
681712
/// Requested reviewer is already assigned to that PR
682713
ReviewerAlreadyAssigned { username: String },
714+
/// Requested reviewer was already assigned previously to that PR.
715+
ReviewerPreviouslyAssigned { username: String },
683716
/// Data required for assignment could not be loaded from the DB.
684717
DatabaseError(String),
685718
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +759,13 @@ impl fmt::Display for FindReviewerError {
726759
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
727760
)
728761
}
762+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
763+
write!(
764+
f,
765+
"{}",
766+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
767+
)
768+
}
729769
FindReviewerError::DatabaseError(error) => {
730770
write!(f, "Database error: {error}")
731771
}
@@ -748,7 +788,7 @@ Please select a different reviewer.",
748788
/// auto-assign groups, or rust-lang team names. It must have at least one
749789
/// entry.
750790
async fn find_reviewer_from_names(
751-
db: &DbClient,
791+
db: &mut DbClient,
752792
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
753793
teams: &Teams,
754794
config: &AssignConfig,
@@ -916,7 +956,7 @@ fn expand_teams_and_groups(
916956
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917957
/// If no reviewer is available, returns an error.
918958
async fn candidate_reviewers_from_names<'a>(
919-
db: &DbClient,
959+
db: &mut DbClient,
920960
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
921961
teams: &'a Teams,
922962
config: &'a AssignConfig,
@@ -937,6 +977,7 @@ async fn candidate_reviewers_from_names<'a>(
937977
// Set of candidate usernames to choose from.
938978
// We go through each expanded candidate and store either success or an error for them.
939979
let mut candidates: Vec<Result<String, FindReviewerError>> = Vec::new();
980+
let previous_reviewer_names = get_previous_reviewer_names(db, issue).await;
940981

941982
// Step 2: pre-filter candidates based on checks that we can perform quickly
942983
for reviewer_candidate in expanded {
@@ -949,6 +990,8 @@ async fn candidate_reviewers_from_names<'a>(
949990
.iter()
950991
.any(|assignee| name_lower == assignee.login.to_lowercase());
951992

993+
let is_previously_assigned = previous_reviewer_names.contains(&name_lower);
994+
952995
// Record the reason why the candidate was filtered out
953996
let reason = {
954997
if is_pr_author {
@@ -963,6 +1006,14 @@ async fn candidate_reviewers_from_names<'a>(
9631006
Some(FindReviewerError::ReviewerAlreadyAssigned {
9641007
username: candidate.clone(),
9651008
})
1009+
} else if reviewer_candidate.origin == ReviewerCandidateOrigin::Expanded
1010+
&& is_previously_assigned
1011+
{
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_REVIEWERS_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: 71 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

@@ -71,15 +72,28 @@ impl AssignCtx {
7172
self
7273
}
7374

75+
async fn set_previous_reviewers(mut self, users: HashSet<&User>) -> Self {
76+
let mut db = self.test_ctx.db_client_mut();
77+
let mut state: IssueData<'_, Reviewers> =
78+
IssueData::load(&mut db, &self.issue, PREVIOUS_REVIEWERS_KEY)
79+
.await
80+
.unwrap();
81+
82+
// Create a new set with all user names (overwrite existing data)
83+
state.data.names = users.iter().map(|user| user.login.to_lowercase()).collect();
84+
state.save().await.unwrap();
85+
self
86+
}
87+
7488
async fn check(
75-
self,
89+
mut self,
7690
names: &[&str],
7791
expected: Result<&[&str], FindReviewerError>,
7892
) -> anyhow::Result<TestContext> {
7993
let names: Vec<_> = names.iter().map(|n| n.to_string()).collect();
8094

8195
let reviewers = candidate_reviewers_from_names(
82-
self.test_ctx.db_client(),
96+
self.test_ctx.db_client_mut(),
8397
Arc::new(RwLock::new(self.reviewer_workqueue)),
8498
&self.teams,
8599
&self.config,
@@ -527,3 +541,58 @@ async fn vacation() {
527541
})
528542
.await;
529543
}
544+
545+
#[tokio::test]
546+
async fn previous_reviewers_ignore_in_team_success() {
547+
let teams = toml::toml!(compiler = ["martin", "jyn514"]);
548+
let config = toml::Table::new();
549+
run_db_test(|ctx| async move {
550+
let user = user("martin", 1);
551+
basic_test(ctx, config, issue().call())
552+
.teams(&teams)
553+
.set_previous_reviewers(HashSet::from([&user]))
554+
.await
555+
.check(&["compiler"], Ok(&["jyn514"]))
556+
.await
557+
})
558+
.await;
559+
}
560+
561+
#[tokio::test]
562+
async fn previous_reviewers_ignore_in_team_failed() {
563+
let teams = toml::toml!(compiler = ["martin", "jyn514"]);
564+
let config = toml::Table::new();
565+
run_db_test(|ctx| async move {
566+
let user1 = user("martin", 1);
567+
let user2 = user("jyn514", 2);
568+
basic_test(ctx, config, issue().call())
569+
.teams(&teams)
570+
.set_previous_reviewers(HashSet::from([&user1, &user2]))
571+
.await
572+
.check(
573+
&["compiler"],
574+
Err(FindReviewerError::NoReviewer {
575+
initial: vec!["compiler".to_string()],
576+
}),
577+
)
578+
.await
579+
})
580+
.await
581+
}
582+
583+
#[tokio::test]
584+
async fn previous_reviewers_direct_assignee() {
585+
let teams = toml::toml!(compiler = ["martin", "jyn514"]);
586+
let config = toml::Table::new();
587+
run_db_test(|ctx| async move {
588+
let user1 = user("martin", 1);
589+
let user2 = user("jyn514", 2);
590+
basic_test(ctx, config, issue().call())
591+
.teams(&teams)
592+
.set_previous_reviewers(HashSet::from([&user1, &user2]))
593+
.await
594+
.check(&["jyn514"], Ok(&["jyn514"]))
595+
.await
596+
})
597+
.await
598+
}

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)