Skip to content

Commit bb75bca

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

File tree

1 file changed

+82
-29
lines changed

1 file changed

+82
-29
lines changed

src/handlers/assign.rs

Lines changed: 82 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,12 @@ 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+
state.data.names.insert(username.to_string());
272+
state.save().await?;
273+
Ok(())
247274
}
248275

249276
/// Determines who to assign the PR to based on either an `r?` command, or
@@ -261,11 +288,10 @@ async fn determine_assignee(
261288
config: &AssignConfig,
262289
diff: &[FileDiff],
263290
) -> anyhow::Result<(Option<String>, bool)> {
264-
let db_client = ctx.db.get().await;
265291
let teams = crate::team_data::teams(&ctx.github).await?;
266292
if let Some(name) = find_assign_command(ctx, event) {
267293
// User included `r?` in the opening PR body.
268-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, &[name]).await {
294+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, &[name]).await {
269295
Ok(assignee) => return Ok((Some(assignee), true)),
270296
Err(e) => {
271297
event
@@ -279,7 +305,7 @@ async fn determine_assignee(
279305
// Errors fall-through to try fallback group.
280306
match find_reviewers_from_diff(config, diff) {
281307
Ok(candidates) if !candidates.is_empty() => {
282-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, &candidates)
308+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, &candidates)
283309
.await
284310
{
285311
Ok(assignee) => return Ok((Some(assignee), false)),
@@ -290,9 +316,11 @@ async fn determine_assignee(
290316
),
291317
Err(
292318
e @ FindReviewerError::NoReviewer { .. }
319+
// TODO: only NoReviewer can be reached here!
293320
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
294321
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
295-
| e @ FindReviewerError::ReviewerOnVacation { .. },
322+
| e @ FindReviewerError::ReviewerOnVacation { .. }
323+
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. },
296324
) => log::trace!(
297325
"no reviewer could be determined for PR {}: {e}",
298326
event.issue.global_id()
@@ -310,7 +338,7 @@ async fn determine_assignee(
310338
}
311339

312340
if let Some(fallback) = config.adhoc_groups.get("fallback") {
313-
match find_reviewer_from_names(&db_client, &teams, config, &event.issue, fallback).await {
341+
match find_reviewer_from_names(&ctx, &teams, config, &event.issue, fallback).await {
314342
Ok(assignee) => return Ok((Some(assignee), false)),
315343
Err(e) => {
316344
log::trace!(
@@ -485,24 +513,18 @@ pub(super) async fn handle_command(
485513
return Ok(());
486514
}
487515

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-
};
516+
let assignee =
517+
match find_reviewer_from_names(ctx, &teams, config, issue, &[assignee.to_string()])
518+
.await
519+
{
520+
Ok(assignee) => assignee,
521+
Err(e) => {
522+
issue.post_comment(&ctx.github, &e.to_string()).await?;
523+
return Ok(());
524+
}
525+
};
504526

505-
set_assignee(issue, &ctx.github, &assignee).await;
527+
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
506528
} else {
507529
let e = EditIssueBody::new(&issue, "ASSIGN");
508530

@@ -612,6 +634,8 @@ pub enum FindReviewerError {
612634
ReviewerIsPrAuthor { username: String },
613635
/// Requested reviewer is already assigned to that PR
614636
ReviewerAlreadyAssigned { username: String },
637+
/// Requested reviewer is already assigned previously to that PR
638+
ReviewerPreviouslyAssigned { username: String },
615639
}
616640

617641
impl std::error::Error for FindReviewerError {}
@@ -654,6 +678,13 @@ impl fmt::Display for FindReviewerError {
654678
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
655679
)
656680
}
681+
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
682+
write!(
683+
f,
684+
"{}",
685+
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
686+
)
687+
}
657688
}
658689
}
659690
}
@@ -665,7 +696,7 @@ impl fmt::Display for FindReviewerError {
665696
/// auto-assign groups, or rust-lang team names. It must have at least one
666697
/// entry.
667698
async fn find_reviewer_from_names(
668-
_db: &DbClient,
699+
ctx: &Context,
669700
teams: &Teams,
670701
config: &AssignConfig,
671702
issue: &Issue,
@@ -678,7 +709,7 @@ async fn find_reviewer_from_names(
678709
}
679710
}
680711

681-
let candidates = candidate_reviewers_from_names(teams, config, issue, names)?;
712+
let candidates = candidate_reviewers_from_names(ctx, teams, config, issue, names).await?;
682713
// This uses a relatively primitive random choice algorithm.
683714
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
684715
//
@@ -782,7 +813,8 @@ fn expand_teams_and_groups(
782813

783814
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
784815
/// If not reviewer is available, returns an error.
785-
fn candidate_reviewers_from_names<'a>(
816+
async fn candidate_reviewers_from_names<'a>(
817+
ctx: &Context,
786818
teams: &'a Teams,
787819
config: &'a AssignConfig,
788820
issue: &Issue,
@@ -804,6 +836,9 @@ fn candidate_reviewers_from_names<'a>(
804836
.iter()
805837
.any(|assignee| name_lower == assignee.login.to_lowercase());
806838

839+
let previous_reviewer_names = get_previous_reviewer_names(ctx, issue).await;
840+
let is_previously_assigned = previous_reviewer_names.contains(&candidate);
841+
807842
// Record the reason why the candidate was filtered out
808843
let reason = {
809844
if is_pr_author {
@@ -818,6 +853,12 @@ fn candidate_reviewers_from_names<'a>(
818853
Some(FindReviewerError::ReviewerAlreadyAssigned {
819854
username: candidate.clone(),
820855
})
856+
} else if expansion_happened && is_previously_assigned {
857+
// **Only** when r? group is expanded, we consider the reviewer previously assigned
858+
// `r? @reviewer` will not consider the reviewer previously assigned
859+
Some(FindReviewerError::ReviewerPreviouslyAssigned {
860+
username: candidate.clone(),
861+
})
821862
} else {
822863
None
823864
}
@@ -863,3 +904,15 @@ fn candidate_reviewers_from_names<'a>(
863904
Ok(valid_candidates)
864905
}
865906
}
907+
908+
async fn get_previous_reviewer_names(ctx: &Context, issue: &Issue) -> HashSet<String> {
909+
// Get the state of the warnings for this PR in the database.
910+
let mut db = ctx.db.get().await;
911+
let state: IssueData<'_, Reviewer> =
912+
match IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await {
913+
Ok(state) => state,
914+
Err(_) => return HashSet::new(),
915+
};
916+
917+
state.data.names
918+
}

0 commit comments

Comments
 (0)