Skip to content

Commit cbbb5af

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 cbbb5af

File tree

2 files changed

+74
-31
lines changed

2 files changed

+74
-31
lines changed

src/handlers/assign.rs

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
//! `assign.owners` config, it will auto-select an assignee based on the files
2121
//! the PR modifies.
2222
23-
use crate::db::review_prefs::{get_review_prefs_batch, RotationMode};
23+
use crate::db::issue_data::IssueData;
24+
use crate::db::review_prefs::get_review_prefs_batch;
2425
use crate::github::UserId;
25-
use crate::handlers::pr_tracking::ReviewerWorkqueue;
2626
use crate::{
2727
config::AssignConfig,
2828
github::{self, Event, FileDiff, Issue, IssuesAction, Selection},
@@ -36,9 +36,6 @@ use rand::seq::IteratorRandom;
3636
use rust_team_data::v1::Teams;
3737
use std::collections::{HashMap, HashSet};
3838
use std::fmt;
39-
use std::sync::Arc;
40-
use tokio::sync::RwLock;
41-
use tokio_postgres::Client as DbClient;
4239
use tracing as log;
4340

4441
#[cfg(test)]
@@ -92,9 +89,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
9289
9390
Please choose another assignee.";
9491

92+
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewers are assigned before.
93+
94+
Please choose another assignee by using `r? @reviewer`.";
95+
9596
// Special account that we use to prevent assignment.
9697
const GHOST_ACCOUNT: &str = "ghost";
9798

99+
/// Key for the state in the database
100+
const PREVIOUS_REVIEWER_KEY: &str = "previous-reviewer";
101+
102+
/// State stored in the database
103+
#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize, serde::Serialize)]
104+
struct Reviewers {
105+
/// List of the last warnings in the most recent comment.
106+
names: HashSet<String>,
107+
}
108+
98109
/// Assignment data stored in the issue/PR body.
99110
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
100111
struct AssignData {
@@ -217,7 +228,7 @@ pub(super) async fn handle_input(
217228
None
218229
};
219230
if let Some(assignee) = assignee {
220-
set_assignee(&event.issue, &ctx.github, &assignee).await;
231+
set_assignee(&ctx, &event.issue, &ctx.github, &assignee).await?;
221232
}
222233

223234
if let Some(welcome) = welcome {
@@ -249,15 +260,19 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
249260
}
250261

251262
/// Sets the assignee of a PR, alerting any errors.
252-
async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
263+
async fn set_assignee(ctx: &Context, issue: &Issue, github: &GithubClient, username: &str)-> anyhow::Result<()>{
264+
let mut db = ctx.db.get().await;
265+
let mut state: IssueData<'_, Reviewers> =
266+
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await?;
267+
253268
// Don't re-assign if already assigned, e.g. on comment edit
254269
if issue.contain_assignee(&username) {
255270
log::trace!(
256271
"ignoring assign PR {} to {}, already assigned",
257272
issue.global_id(),
258273
username,
259274
);
260-
return;
275+
return Ok(());
261276
}
262277
if let Err(err) = issue.set_assignee(github, &username).await {
263278
log::warn!(
@@ -280,8 +295,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
280295
.await
281296
{
282297
log::warn!("failed to post error comment: {e}");
298+
return Err(e);
283299
}
284300
}
301+
302+
// Record the reviewer in the database
303+
state.data.names.insert(username.to_string());
304+
state.save().await?;
305+
Ok(())
285306
}
286307

287308
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -300,13 +321,11 @@ async fn determine_assignee(
300321
config: &AssignConfig,
301322
diff: &[FileDiff],
302323
) -> anyhow::Result<(Option<String>, bool)> {
303-
let db_client = ctx.db.get().await;
304324
let teams = crate::team_data::teams(&ctx.github).await?;
305325
if let Some(name) = assign_command {
306326
// User included `r?` in the opening PR body.
307327
match find_reviewer_from_names(
308-
&db_client,
309-
ctx.workqueue.clone(),
328+
ctx,
310329
&teams,
311330
config,
312331
&event.issue,
@@ -328,8 +347,7 @@ async fn determine_assignee(
328347
match find_reviewers_from_diff(config, diff) {
329348
Ok(candidates) if !candidates.is_empty() => {
330349
match find_reviewer_from_names(
331-
&db_client,
332-
ctx.workqueue.clone(),
350+
ctx,
333351
&teams,
334352
config,
335353
&event.issue,
@@ -347,6 +365,7 @@ async fn determine_assignee(
347365
e @ FindReviewerError::NoReviewer { .. }
348366
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
349367
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
368+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
350369
| e @ FindReviewerError::ReviewerOffRotation { .. }
351370
| e @ FindReviewerError::DatabaseError(_)
352371
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
@@ -368,8 +387,7 @@ async fn determine_assignee(
368387

369388
if let Some(fallback) = config.adhoc_groups.get("fallback") {
370389
match find_reviewer_from_names(
371-
&db_client,
372-
ctx.workqueue.clone(),
390+
ctx,
373391
&teams,
374392
config,
375393
&event.issue,
@@ -551,10 +569,8 @@ pub(super) async fn handle_command(
551569
return Ok(());
552570
}
553571

554-
let db_client = ctx.db.get().await;
555572
let assignee = match find_reviewer_from_names(
556-
&db_client,
557-
ctx.workqueue.clone(),
573+
ctx,
558574
&teams,
559575
config,
560576
issue,
@@ -569,7 +585,7 @@ pub(super) async fn handle_command(
569585
}
570586
};
571587

572-
set_assignee(issue, &ctx.github, &assignee).await;
588+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
573589
} else {
574590
let e = EditIssueBody::new(&issue, "ASSIGN");
575591

@@ -680,6 +696,8 @@ enum FindReviewerError {
680696
ReviewerIsPrAuthor { username: String },
681697
/// Requested reviewer is already assigned to that PR
682698
ReviewerAlreadyAssigned { username: String },
699+
/// Requested reviewer is already assigned previously to that PR.
700+
ReviewerPreviouslyAssigned { username: String },
683701
/// Data required for assignment could not be loaded from the DB.
684702
DatabaseError(String),
685703
/// The reviewer has too many PRs alreayd assigned.
@@ -726,6 +744,13 @@ impl fmt::Display for FindReviewerError {
726744
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
727745
)
728746
}
747+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
748+
write!(
749+
f,
750+
"{}",
751+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
752+
)
753+
}
729754
FindReviewerError::DatabaseError(error) => {
730755
write!(f, "Database error: {error}")
731756
}
@@ -748,8 +773,7 @@ Please select a different reviewer.",
748773
/// auto-assign groups, or rust-lang team names. It must have at least one
749774
/// entry.
750775
async fn find_reviewer_from_names(
751-
db: &DbClient,
752-
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
776+
ctx: &Context,
753777
teams: &Teams,
754778
config: &AssignConfig,
755779
issue: &Issue,
@@ -763,7 +787,7 @@ async fn find_reviewer_from_names(
763787
}
764788

765789
let candidates =
766-
candidate_reviewers_from_names(db, workqueue, teams, config, issue, names).await?;
790+
candidate_reviewers_from_names(ctx, teams, config, issue, names).await?;
767791
assert!(!candidates.is_empty());
768792

769793
// This uses a relatively primitive random choice algorithm.
@@ -916,15 +940,15 @@ fn expand_teams_and_groups(
916940
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
917941
/// If no reviewer is available, returns an error.
918942
async fn candidate_reviewers_from_names<'a>(
919-
db: &DbClient,
920-
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
943+
ctx: &Context,
921944
teams: &'a Teams,
922945
config: &'a AssignConfig,
923946
issue: &Issue,
924947
names: &'a [String],
925948
) -> Result<HashSet<String>, FindReviewerError> {
926949
// Step 1: expand teams and groups into candidate names
927950
let expanded = expand_teams_and_groups(teams, issue, config, names)?;
951+
let expansion_happend = expanded.iter().any(|c| c.origin == ReviewerCandidateOrigin::Expanded);
928952
let expanded_count = expanded.len();
929953

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

941966
// Step 2: pre-filter candidates based on checks that we can perform quickly
942967
for reviewer_candidate in expanded {
@@ -949,6 +974,8 @@ async fn candidate_reviewers_from_names<'a>(
949974
.iter()
950975
.any(|assignee| name_lower == assignee.login.to_lowercase());
951976

977+
let is_previously_assigned = previous_reviewer_names.contains(&reviewer_candidate.name);
978+
952979
// Record the reason why the candidate was filtered out
953980
let reason = {
954981
if is_pr_author {
@@ -963,6 +990,12 @@ async fn candidate_reviewers_from_names<'a>(
963990
Some(FindReviewerError::ReviewerAlreadyAssigned {
964991
username: candidate.clone(),
965992
})
993+
} else if expansion_happend && is_previously_assigned {
994+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
995+
// `r? @reviewer` will not consider the reviewer previously assigned
996+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
997+
username: candidate.clone(),
998+
})
966999
} else {
9671000
None
9681001
}
@@ -983,12 +1016,14 @@ async fn candidate_reviewers_from_names<'a>(
9831016
.filter_map(|res| res.as_deref().ok().map(|s| s.to_string()))
9841017
.collect();
9851018
let usernames: Vec<&str> = usernames.iter().map(|s| s.as_str()).collect();
986-
let review_prefs = get_review_prefs_batch(db, &usernames)
1019+
1020+
let db_client = ctx.db.get().await;
1021+
let review_prefs = get_review_prefs_batch(&db_client, &usernames)
9871022
.await
9881023
.context("cannot fetch review preferences")
9891024
.map_err(|e| FindReviewerError::DatabaseError(e.to_string()))?;
9901025

991-
let workqueue = workqueue.read().await;
1026+
let workqueue = ctx.workqueue.read().await;
9921027

9931028
// Step 4: check review preferences
9941029
candidates = candidates
@@ -1009,9 +1044,6 @@ async fn candidate_reviewers_from_names<'a>(
10091044
return Err(FindReviewerError::ReviewerAtMaxCapacity { username });
10101045
}
10111046
}
1012-
if review_prefs.rotation_mode == RotationMode::OffRotation {
1013-
return Err(FindReviewerError::ReviewerOffRotation { username });
1014-
}
10151047

10161048
return Ok(username);
10171049
})
@@ -1058,3 +1090,14 @@ async fn candidate_reviewers_from_names<'a>(
10581090
.collect())
10591091
}
10601092
}
1093+
1094+
async fn get_previous_reviewer_names(ctx: &Context, issue: &Issue) -> HashSet<String> {
1095+
let mut db = ctx.db.get().await;
1096+
let state: IssueData<'_, Reviewers> =
1097+
match IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await {
1098+
Ok(state) => state,
1099+
Err(_) => return HashSet::new(),
1100+
};
1101+
1102+
state.data.names
1103+
}

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)