Skip to content

Commit 4c97007

Browse files
committed
Pick reviewer which was not assigned before **only** when r? group
Signed-off-by: xizheyin <[email protected]>
1 parent 527bf21 commit 4c97007

File tree

2 files changed

+186
-84
lines changed

2 files changed

+186
-84
lines changed

src/handlers/assign.rs

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
2323
use crate::{
2424
config::AssignConfig,
25+
db::issue_data::IssueData,
2526
github::{self, Event, FileDiff, Issue, IssuesAction, Selection},
2627
handlers::{Context, GithubClient, IssuesEvent},
2728
interactions::EditIssueBody,
@@ -33,7 +34,6 @@ use rand::seq::IteratorRandom;
3334
use rust_team_data::v1::Teams;
3435
use std::collections::{HashMap, HashSet};
3536
use std::fmt;
36-
use tokio_postgres::Client as DbClient;
3737
use tracing as log;
3838

3939
#[cfg(test)]
@@ -87,9 +87,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =
8787
8888
Please choose another assignee.";
8989

90+
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewers are assigned before.
91+
92+
Please choose another assignee by using `r? @reviewer`.";
93+
9094
// Special account that we use to prevent assignment.
9195
const GHOST_ACCOUNT: &str = "ghost";
9296

97+
/// Key for the state in the database
98+
const PREVIOUS_REVIEWER_KEY: &str = "previous-reviewer";
99+
100+
/// State stored in the database
101+
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
102+
struct Reviewer {
103+
/// List of the last warnings in the most recent comment.
104+
names: HashSet<String>,
105+
}
106+
93107
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94108
struct AssignData {
95109
user: Option<String>,
@@ -179,7 +193,7 @@ pub(super) async fn handle_input(
179193
None
180194
};
181195
if let Some(assignee) = assignee {
182-
set_assignee(&event.issue, &ctx.github, &assignee).await;
196+
set_assignee(&ctx, &event.issue, &ctx.github, &assignee).await?;
183197
}
184198

185199
if let Some(welcome) = welcome {
@@ -211,15 +225,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
211225
}
212226

213227
/// Sets the assignee of a PR, alerting any errors.
214-
async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
228+
async fn set_assignee(
229+
ctx: &Context,
230+
issue: &Issue,
231+
github: &GithubClient,
232+
username: &str,
233+
) -> anyhow::Result<()> {
234+
let mut db = ctx.db.get().await;
235+
let mut state: IssueData<'_, Reviewer> =
236+
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await?;
237+
215238
// Don't re-assign if already assigned, e.g. on comment edit
216239
if issue.contain_assignee(&username) {
217240
log::trace!(
218241
"ignoring assign PR {} to {}, already assigned",
219242
issue.global_id(),
220243
username,
221244
);
222-
return;
245+
return Ok(());
223246
}
224247
if let Err(err) = issue.set_assignee(github, &username).await {
225248
log::warn!(
@@ -242,8 +265,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
242265
.await
243266
{
244267
log::warn!("failed to post error comment: {e}");
268+
return Err(e);
245269
}
246270
}
271+
272+
state.data.names.insert(username.to_string());
273+
state.save().await?;
274+
275+
Ok(())
247276
}
248277

249278
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -261,11 +290,10 @@ async fn determine_assignee(
261290
config: &AssignConfig,
262291
diff: &[FileDiff],
263292
) -> anyhow::Result<(Option<String>, bool)> {
264-
let db_client = ctx.db.get().await;
265293
let teams = crate::team_data::teams(&ctx.github).await?;
266294
if let Some(name) = find_assign_command(ctx, event) {
267295
// User included `r?` in the opening PR body.
268-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, &[name]).await {
296+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, &[name]).await {
269297
Ok(assignee) => return Ok((Some(assignee), true)),
270298
Err(e) => {
271299
event
@@ -279,7 +307,7 @@ async fn determine_assignee(
279307
// Errors fall-through to try fallback group.
280308
match find_reviewers_from_diff(config, diff) {
281309
Ok(candidates) if !candidates.is_empty() => {
282-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, &candidates)
310+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, &candidates)
283311
.await
284312
{
285313
Ok(assignee) => return Ok((Some(assignee), false)),
@@ -290,9 +318,11 @@ async fn determine_assignee(
290318
),
291319
Err(
292320
e @ FindReviewerError::NoReviewer { .. }
321+
// TODO: only NoReviewer can be reached here!
293322
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
294323
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
295-
| e @ FindReviewerError::ReviewerOnVacation { .. },
324+
| e @ FindReviewerError::ReviewerOnVacation { .. }
325+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. },
296326
) => log::trace!(
297327
"no reviewer could be determined for PR {}: {e}",
298328
event.issue.global_id()
@@ -310,7 +340,7 @@ async fn determine_assignee(
310340
}
311341

312342
if let Some(fallback) = config.adhoc_groups.get("fallback") {
313-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, fallback).await {
343+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, fallback).await {
314344
Ok(assignee) => return Ok((Some(assignee), false)),
315345
Err(e) => {
316346
log::trace!(
@@ -485,24 +515,18 @@ pub(super) async fn handle_command(
485515
return Ok(());
486516
}
487517

488-
let db_client = ctx.db.get().await;
489-
let assignee = match find_reviewer_from_names(
490-
&db_client,
491-
&teams,
492-
config,
493-
issue,
494-
&[assignee.to_string()],
495-
)
496-
.await
497-
{
498-
Ok(assignee) => assignee,
499-
Err(e) => {
500-
issue.post_comment(&ctx.github, &e.to_string()).await?;
501-
return Ok(());
502-
}
503-
};
518+
let assignee =
519+
match find_reviewer_from_names(ctx, &teams, config, issue, &[assignee.to_string()])
520+
.await
521+
{
522+
Ok(assignee) => assignee,
523+
Err(e) => {
524+
issue.post_comment(&ctx.github, &e.to_string()).await?;
525+
return Ok(());
526+
}
527+
};
504528

505-
set_assignee(issue, &ctx.github, &assignee).await;
529+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
506530
} else {
507531
let e = EditIssueBody::new(&issue, "ASSIGN");
508532

@@ -612,6 +636,8 @@ pub enum FindReviewerError {
612636
ReviewerIsPrAuthor { username: String },
613637
/// Requested reviewer is already assigned to that PR
614638
ReviewerAlreadyAssigned { username: String },
639+
/// Requested reviewer is already assigned previously to that PR
640+
ReviewerPreviouslyAssigned { username: String },
615641
}
616642

617643
impl std::error::Error for FindReviewerError {}
@@ -654,6 +680,13 @@ impl fmt::Display for FindReviewerError {
654680
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
655681
)
656682
}
683+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
684+
write!(
685+
f,
686+
"{}",
687+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
688+
)
689+
}
657690
}
658691
}
659692
}
@@ -665,7 +698,7 @@ impl fmt::Display for FindReviewerError {
665698
/// auto-assign groups, or rust-lang team names. It must have at least one
666699
/// entry.
667700
async fn find_reviewer_from_names(
668-
_db: &DbClient,
701+
ctx: &Context,
669702
teams: &Teams,
670703
config: &AssignConfig,
671704
issue: &Issue,
@@ -678,7 +711,7 @@ async fn find_reviewer_from_names(
678711
}
679712
}
680713

681-
let candidates = candidate_reviewers_from_names(teams, config, issue, names)?;
714+
let candidates = candidate_reviewers_from_names(ctx, teams, config, issue, names).await?;
682715
// This uses a relatively primitive random choice algorithm.
683716
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
684717
//
@@ -782,7 +815,8 @@ fn expand_teams_and_groups(
782815

783816
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
784817
/// If not reviewer is available, returns an error.
785-
fn candidate_reviewers_from_names<'a>(
818+
async fn candidate_reviewers_from_names<'a>(
819+
ctx: &Context,
786820
teams: &'a Teams,
787821
config: &'a AssignConfig,
788822
issue: &Issue,
@@ -804,6 +838,9 @@ fn candidate_reviewers_from_names<'a>(
804838
.iter()
805839
.any(|assignee| name_lower == assignee.login.to_lowercase());
806840

841+
let previous_reviewer_names = get_previous_reviewer_names(ctx, issue).await;
842+
let is_previously_assigned = previous_reviewer_names.contains(&candidate);
843+
807844
// Record the reason why the candidate was filtered out
808845
let reason = {
809846
if is_pr_author {
@@ -818,6 +855,12 @@ fn candidate_reviewers_from_names<'a>(
818855
Some(FindReviewerError::ReviewerAlreadyAssigned {
819856
username: candidate.clone(),
820857
})
858+
} else if expansion_happened && is_previously_assigned {
859+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
860+
// `r? @reviewer` will not consider the reviewer previously assigned
861+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
862+
username: candidate.clone(),
863+
})
821864
} else {
822865
None
823866
}
@@ -863,3 +906,19 @@ fn candidate_reviewers_from_names<'a>(
863906
Ok(valid_candidates)
864907
}
865908
}
909+
910+
async fn get_previous_reviewer_names(ctx: &Context, issue: &Issue) -> HashSet<String> {
911+
if cfg!(test) {
912+
return HashSet::new();
913+
}
914+
915+
// Get the state of the warnings for this PR in the database.
916+
let mut db = ctx.db.get().await;
917+
let state: IssueData<'_, Reviewer> =
918+
match IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await {
919+
Ok(state) => state,
920+
Err(_) => return HashSet::new(),
921+
};
922+
923+
state.data.names
924+
}

0 commit comments

Comments
 (0)