Skip to content

Pick reviewer who is not previous assignee when r? group #1958

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 74 additions & 13 deletions src/handlers/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! `assign.owners` config, it will auto-select an assignee based on the files
//! the PR modifies.

use crate::db::issue_data::IssueData;
use crate::db::review_prefs::{get_review_prefs_batch, RotationMode};
use crate::github::UserId;
use crate::handlers::pr_tracking::ReviewerWorkqueue;
Expand Down Expand Up @@ -92,9 +93,23 @@ const REVIEWER_ALREADY_ASSIGNED: &str =

Please choose another assignee.";

const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewers are assigned before.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewers are assigned before.
const REVIEWER_ASSIGNED_BEFORE: &str = "Requested reviewer(s) were already assigned before.


Please choose another assignee by using `r? @reviewer`.";

// Special account that we use to prevent assignment.
const GHOST_ACCOUNT: &str = "ghost";

/// Key for the state in the database
const PREVIOUS_REVIEWER_KEY: &str = "previous-reviewer";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const PREVIOUS_REVIEWER_KEY: &str = "previous-reviewer";
const PREVIOUS_REVIEWERS_KEY: &str = "previous-reviewers";


/// State stored in the database
#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize, serde::Serialize)]
struct Reviewers {
/// List of the last warnings in the most recent comment.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong comment.

names: HashSet<String>,
}

/// Assignment data stored in the issue/PR body.
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct AssignData {
Expand Down Expand Up @@ -217,7 +232,7 @@ pub(super) async fn handle_input(
None
};
if let Some(assignee) = assignee {
set_assignee(&event.issue, &ctx.github, &assignee).await;
set_assignee(&ctx, &event.issue, &ctx.github, &assignee).await?;
}

if let Some(welcome) = welcome {
Expand Down Expand Up @@ -249,15 +264,24 @@ fn is_self_assign(assignee: &str, pr_author: &str) -> bool {
}

/// Sets the assignee of a PR, alerting any errors.
async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
async fn set_assignee(
ctx: &Context,
issue: &Issue,
github: &GithubClient,
username: &str,
) -> anyhow::Result<()> {
let mut db = ctx.db.get().await;
let mut state: IssueData<'_, Reviewers> =
IssueData::load(&mut db, &issue, PREVIOUS_REVIEWER_KEY).await?;

// Don't re-assign if already assigned, e.g. on comment edit
if issue.contain_assignee(&username) {
log::trace!(
"ignoring assign PR {} to {}, already assigned",
issue.global_id(),
username,
);
return;
return Ok(());
}
if let Err(err) = issue.set_assignee(github, &username).await {
log::warn!(
Expand All @@ -280,8 +304,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
.await
{
log::warn!("failed to post error comment: {e}");
return Err(e);
}
}

// Record the reviewer in the database
state.data.names.insert(username.to_string());
state.save().await?;
Ok(())
}

/// Determines who to assign the PR to based on either an `r?` command, or
Expand All @@ -300,12 +330,12 @@ async fn determine_assignee(
config: &AssignConfig,
diff: &[FileDiff],
) -> anyhow::Result<(Option<String>, bool)> {
let db_client = ctx.db.get().await;
let mut db_client = ctx.db.get().await;
let teams = crate::team_data::teams(&ctx.github).await?;
if let Some(name) = assign_command {
// User included `r?` in the opening PR body.
match find_reviewer_from_names(
&db_client,
&mut db_client,
ctx.workqueue.clone(),
&teams,
config,
Expand All @@ -328,7 +358,7 @@ async fn determine_assignee(
match find_reviewers_from_diff(config, diff) {
Ok(candidates) if !candidates.is_empty() => {
match find_reviewer_from_names(
&db_client,
&mut db_client,
ctx.workqueue.clone(),
&teams,
config,
Expand All @@ -347,6 +377,7 @@ async fn determine_assignee(
e @ FindReviewerError::NoReviewer { .. }
| e @ FindReviewerError::ReviewerIsPrAuthor { .. }
| e @ FindReviewerError::ReviewerAlreadyAssigned { .. }
| e @ FindReviewerError::ReviewerPreviouslyAssigned { .. }
| e @ FindReviewerError::ReviewerOffRotation { .. }
| e @ FindReviewerError::DatabaseError(_)
| e @ FindReviewerError::ReviewerAtMaxCapacity { .. },
Expand All @@ -368,7 +399,7 @@ async fn determine_assignee(

if let Some(fallback) = config.adhoc_groups.get("fallback") {
match find_reviewer_from_names(
&db_client,
&mut db_client,
ctx.workqueue.clone(),
&teams,
config,
Expand Down Expand Up @@ -550,10 +581,9 @@ pub(super) async fn handle_command(
issue.remove_assignees(&ctx.github, Selection::All).await?;
return Ok(());
}

let db_client = ctx.db.get().await;
let mut db_client = ctx.db.get().await;
let assignee = match find_reviewer_from_names(
&db_client,
&mut db_client,
ctx.workqueue.clone(),
&teams,
config,
Expand All @@ -569,7 +599,7 @@ pub(super) async fn handle_command(
}
};

set_assignee(issue, &ctx.github, &assignee).await;
set_assignee(ctx, issue, &ctx.github, &assignee).await?;
} else {
let e = EditIssueBody::new(&issue, "ASSIGN");

Expand Down Expand Up @@ -680,6 +710,8 @@ enum FindReviewerError {
ReviewerIsPrAuthor { username: String },
/// Requested reviewer is already assigned to that PR
ReviewerAlreadyAssigned { username: String },
/// Requested reviewer is already assigned previously to that PR.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Requested reviewer is already assigned previously to that PR.
/// Requested reviewer was already assigned previously to that PR.

ReviewerPreviouslyAssigned { username: String },
/// Data required for assignment could not be loaded from the DB.
DatabaseError(String),
/// The reviewer has too many PRs alreayd assigned.
Expand Down Expand Up @@ -726,6 +758,13 @@ impl fmt::Display for FindReviewerError {
REVIEWER_ALREADY_ASSIGNED.replace("{username}", username)
)
}
FindReviewerError::ReviewerPreviouslyAssigned { username } => {
write!(
f,
"{}",
REVIEWER_ASSIGNED_BEFORE.replace("{username}", username)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEWER_ASSIGNED_BEFORE doesn't seem to contain {username}? It shouldn't need any replacement.

)
}
FindReviewerError::DatabaseError(error) => {
write!(f, "Database error: {error}")
}
Expand All @@ -748,7 +787,7 @@ Please select a different reviewer.",
/// auto-assign groups, or rust-lang team names. It must have at least one
/// entry.
async fn find_reviewer_from_names(
db: &DbClient,
db: &mut DbClient,
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
teams: &Teams,
config: &AssignConfig,
Expand Down Expand Up @@ -916,7 +955,7 @@ fn expand_teams_and_groups(
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
/// If no reviewer is available, returns an error.
async fn candidate_reviewers_from_names<'a>(
db: &DbClient,
db: &mut DbClient,
workqueue: Arc<RwLock<ReviewerWorkqueue>>,
teams: &'a Teams,
config: &'a AssignConfig,
Expand All @@ -925,6 +964,9 @@ async fn candidate_reviewers_from_names<'a>(
) -> Result<HashSet<String>, FindReviewerError> {
// Step 1: expand teams and groups into candidate names
let expanded = expand_teams_and_groups(teams, issue, config, names)?;
let expansion_happend = expanded
.iter()
.any(|c| c.origin == ReviewerCandidateOrigin::Expanded);
let expanded_count = expanded.len();

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

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

let is_previously_assigned = previous_reviewer_names.contains(&reviewer_candidate.name);

// Record the reason why the candidate was filtered out
let reason = {
if is_pr_author {
Expand All @@ -963,6 +1008,12 @@ async fn candidate_reviewers_from_names<'a>(
Some(FindReviewerError::ReviewerAlreadyAssigned {
username: candidate.clone(),
})
} else if expansion_happend && is_previously_assigned {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to check expansion_happened here. We should decide per-reviewer basis if they were expanded or not. It's enough to check reviewer_candidate.origin here.

// **Only** when r? group is expanded, we consider the reviewer previously assigned
// `r? @reviewer` will not consider the reviewer previously assigned
Some(FindReviewerError::ReviewerPreviouslyAssigned {
username: candidate.clone(),
})
} else {
None
}
Expand Down Expand Up @@ -1058,3 +1109,13 @@ async fn candidate_reviewers_from_names<'a>(
.collect())
}
}

async fn get_previous_reviewer_names(db: &mut DbClient, issue: &Issue) -> HashSet<String> {
let state: IssueData<'_, Reviewers> =
match IssueData::load(db, &issue, PREVIOUS_REVIEWER_KEY).await {
Ok(state) => state,
Err(_) => return HashSet::new(),
};

state.data.names
}
5 changes: 3 additions & 2 deletions src/handlers/assign/tests/tests_candidates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use super::super::*;
use crate::db::review_prefs::{upsert_review_prefs, RotationMode};
use crate::github::{PullRequestNumber, User};
use crate::handlers::pr_tracking::ReviewerWorkqueue;
use crate::tests::github::{issue, user};
use crate::tests::{run_db_test, TestContext};

Expand Down Expand Up @@ -72,14 +73,14 @@ impl AssignCtx {
}

async fn check(
self,
mut self,
names: &[&str],
expected: Result<&[&str], FindReviewerError>,
) -> anyhow::Result<TestContext> {
let names: Vec<_> = names.iter().map(|n| n.to_string()).collect();

let reviewers = candidate_reviewers_from_names(
self.test_ctx.db_client(),
self.test_ctx.db_client_mut(),
Arc::new(RwLock::new(self.reviewer_workqueue)),
&self.teams,
&self.config,
Expand Down
4 changes: 4 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl TestContext {
&self.client
}

pub(crate) fn db_client_mut(&mut self) -> &mut PooledClient {
&mut self.client
}

pub(crate) async fn add_user(&self, name: &str, id: u64) {
record_username(self.db_client(), id, name)
.await
Expand Down
Loading