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
89use crate :: {
910 config:: ReviewPrefsConfig ,
1011 db:: notifications:: record_username,
1112 github:: { IssuesAction , IssuesEvent } ,
1213 handlers:: Context ,
14+ ReviewPrefs ,
1315} ;
1416use anyhow:: Context as _;
1517use tokio_postgres:: Client as DbClient ;
1618
19+ use super :: assign:: { FindReviewerError , REVIEWER_HAS_NO_CAPACITY , SELF_ASSIGN_HAS_NO_CAPACITY } ;
20+
1721pub ( super ) struct ReviewPrefsInput { }
1822
1923pub ( 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,61 @@ pub(super) async fn handle_input<'a>(
6670 if matches ! ( event. action, IssuesAction :: Unassigned { .. } ) {
6771 delete_pr_from_workqueue ( & db_client, assignee. id , event. issue . number )
6872 . await
69- . context ( "Failed to remove PR from workqueue " ) ?;
73+ . context ( "Failed to remove PR from work ueue " ) ?;
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 , 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+ // TODO: we should just fetch the number of assigned prs and max assigned prs. The caller should do the check.
109+ pub async fn has_user_capacity (
110+ db : & crate :: db:: PooledClient ,
111+ assignee : & str ,
112+ ) -> anyhow:: Result < ReviewPrefs , FindReviewerError > {
113+ let q = "
114+ SELECT username, r.*
115+ FROM review_prefs r
116+ JOIN users ON users.user_id = r.user_id
117+ WHERE username = $1
118+ AND CARDINALITY(r.assigned_prs) < LEAST(COALESCE(r.max_assigned_prs,1000000));" ;
119+ let rec = db. query_one ( q, & [ & assignee] ) . await ;
120+ if let Err ( _) = rec {
121+ return Err ( FindReviewerError :: ReviewerHasNoCapacity {
122+ username : assignee. to_string ( ) ,
123+ } ) ;
124+ }
125+ Ok ( rec. unwrap ( ) . into ( ) )
126+ }
127+
81128/// Add a PR to the workqueue of a team member.
82129/// Ensures no accidental PR duplicates.
83130async fn upsert_pr_into_workqueue (
0 commit comments