Skip to content

Commit bb17381

Browse files
committed
Ensure user capacity is respected on PR assignment
This check is specifically used when assigning from the Github web UI
1 parent d310b05 commit bb17381

File tree

2 files changed

+53
-7
lines changed

2 files changed

+53
-7
lines changed

src/handlers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ macro_rules! issue_handlers {
159159

160160
// Handle events that happened on issues
161161
//
162-
// This is for events that happen only on issues (e.g. label changes).
162+
// This is for events that happen only on issues or pull requests (e.g. label changes or assignments).
163163
// Each module in the list must contain the functions `parse_input` and `handle_input`.
164164
issue_handlers! {
165165
assign,
@@ -280,7 +280,7 @@ macro_rules! command_handlers {
280280
//
281281
// This is for handlers for commands parsed by the `parser` crate.
282282
// Each variant of `parser::command::Command` must be in this list,
283-
// preceded by the module containing the coresponding `handle_command` function
283+
// preceded by the module containing the corresponding `handle_command` function
284284
command_handlers! {
285285
assign: Assign,
286286
glacier: Glacier,

src/handlers/pr_tracking.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
//! This module updates the PR workqueue of the Rust project contributors
2+
//! Runs after a PR has been assigned or unassigned
23
//!
34
//! Purpose:
45
//!
5-
//! - Adds the PR to the workqueue of one team member (when the PR has been assigned)
6-
//! - Removes the PR from the workqueue of one team member (when the PR is unassigned or closed)
6+
//! - Adds the PR to the workqueue of one team member (after the PR has been assigned)
7+
//! - Removes the PR from the workqueue of one team member (after the PR has been unassigned or closed)
78
89
use crate::{
910
config::ReviewPrefsConfig,
1011
db::notifications::record_username,
1112
github::{IssuesAction, IssuesEvent},
1213
handlers::Context,
14+
ReviewPrefs,
1315
};
1416
use anyhow::Context as _;
1517
use tokio_postgres::Client as DbClient;
1618

19+
use super::assign::{FindReviewerError, REVIEWER_HAS_NO_CAPACITY, SELF_ASSIGN_HAS_NO_CAPACITY};
20+
1721
pub(super) struct ReviewPrefsInput {}
1822

1923
pub(super) async fn parse_input(
@@ -49,7 +53,7 @@ pub(super) async fn handle_input<'a>(
4953
) -> anyhow::Result<()> {
5054
let db_client = ctx.db.get().await;
5155

52-
// extract the assignee matching the assignment or unassignment enum variants or return and ignore this handler
56+
// extract the assignee or ignore this handler and return
5357
let IssuesEvent {
5458
action: IssuesAction::Assigned { assignee } | IssuesAction::Unassigned { assignee },
5559
..
@@ -66,18 +70,60 @@ pub(super) async fn handle_input<'a>(
6670
if matches!(event.action, IssuesAction::Unassigned { .. }) {
6771
delete_pr_from_workqueue(&db_client, assignee.id.unwrap(), event.issue.number)
6872
.await
69-
.context("Failed to remove PR from workqueue")?;
73+
.context("Failed to remove PR from work queue")?;
7074
}
7175

76+
// This handler is reached also when assigning a PR using the Github UI
77+
// (i.e. from the "Assignees" dropdown menu).
78+
// We need to also check assignee availability here.
7279
if matches!(event.action, IssuesAction::Assigned { .. }) {
80+
let work_queue = has_user_capacity(&db_client, &assignee.login)
81+
.await
82+
.context("Failed to retrieve user work queue");
83+
84+
// if user has no capacity, revert the PR assignment (GitHub has already assigned it)
85+
// and post a comment suggesting what to do
86+
if let Err(_) = work_queue {
87+
event
88+
.issue
89+
.remove_assignees(&ctx.github, crate::github::Selection::One(&assignee.login))
90+
.await?;
91+
92+
let msg = if assignee.login.to_lowercase() == event.issue.user.login.to_lowercase() {
93+
SELF_ASSIGN_HAS_NO_CAPACITY.replace("{username}", &assignee.login)
94+
} else {
95+
REVIEWER_HAS_NO_CAPACITY.replace("{username}", &assignee.login)
96+
};
97+
event.issue.post_comment(&ctx.github, &msg).await?;
98+
}
99+
73100
upsert_pr_into_workqueue(&db_client, assignee.id.unwrap(), event.issue.number)
74101
.await
75-
.context("Failed to add PR to workqueue")?;
102+
.context("Failed to add PR to work queue")?;
76103
}
77104

78105
Ok(())
79106
}
80107

108+
pub async fn has_user_capacity(
109+
db: &crate::db::PooledClient,
110+
assignee: &str,
111+
) -> anyhow::Result<ReviewPrefs, FindReviewerError> {
112+
let q = "
113+
SELECT username, r.*
114+
FROM review_prefs r
115+
JOIN users ON users.user_id = r.user_id
116+
WHERE username = $1
117+
AND CARDINALITY(r.assigned_prs) < max_assigned_prs;";
118+
let rec = db.query_one(q, &[&assignee]).await;
119+
if let Err(_) = rec {
120+
return Err(FindReviewerError::ReviewerHasNoCapacity {
121+
username: assignee.to_string(),
122+
});
123+
}
124+
Ok(rec.unwrap().into())
125+
}
126+
81127
/// Add a PR to the workqueue of a team member.
82128
/// Ensures no accidental PR duplicates.
83129
async fn upsert_pr_into_workqueue(

0 commit comments

Comments
 (0)