@@ -4,7 +4,9 @@ mod commands;
44
55use crate :: db:: notifications:: add_metadata;
66use crate :: db:: notifications:: { self , delete_ping, move_indices, record_ping, Identifier } ;
7- use crate :: db:: review_prefs:: { get_review_prefs, upsert_review_prefs, RotationMode } ;
7+ use crate :: db:: review_prefs:: {
8+ get_review_prefs, get_review_prefs_batch, upsert_review_prefs, RotationMode ,
9+ } ;
810use crate :: github:: User ;
911use crate :: handlers:: docs_update:: docs_update;
1012use crate :: handlers:: pr_tracking:: get_assigned_prs;
@@ -17,7 +19,8 @@ use crate::zulip::commands::{
1719 parse_cli, ChatCommand , LookupCmd , PingGoalsArgs , StreamCommand , WorkqueueCmd , WorkqueueLimit ,
1820} ;
1921use anyhow:: { format_err, Context as _} ;
20- use rust_team_data:: v1:: TeamKind ;
22+ use rust_team_data:: v1:: { TeamKind , TeamMember } ;
23+ use std:: cmp:: Reverse ;
2124use std:: fmt:: Write as _;
2225use subtle:: ConstantTimeEq ;
2326use tracing as log;
@@ -201,6 +204,7 @@ async fn handle_command<'a>(
201204 ChatCommand :: Work ( cmd) => workqueue_commands ( ctx, gh_id, cmd) . await ,
202205 ChatCommand :: PingGoals ( args) => ping_goals_cmd ( ctx, gh_id, & args) . await ,
203206 ChatCommand :: DocsUpdate => trigger_docs_update ( message_data, & ctx. zulip ) ,
207+ ChatCommand :: TeamStats { name } => team_status_cmd ( ctx, & name) . await ,
204208 } ;
205209
206210 let output = output?;
@@ -288,6 +292,104 @@ async fn ping_goals_cmd(
288292 }
289293}
290294
295+ async fn team_status_cmd ( ctx : & Context , team_name : & str ) -> anyhow:: Result < Option < String > > {
296+ use std:: fmt:: Write ;
297+
298+ let Some ( team) = ctx. team . get_team ( team_name) . await ? else {
299+ return Ok ( Some ( format ! ( "Team {team_name} not found" ) ) ) ;
300+ } ;
301+
302+ let mut members = team. members ;
303+ members. sort_by ( |a, b| a. github . cmp ( & b. github ) ) ;
304+
305+ let usernames: Vec < & str > = members
306+ . iter ( )
307+ . map ( |member| member. github . as_str ( ) )
308+ . collect ( ) ;
309+
310+ let db = ctx. db . get ( ) . await ;
311+ let review_prefs = get_review_prefs_batch ( & db, & usernames)
312+ . await
313+ . context ( "cannot load review preferences" ) ?;
314+
315+ let workqueue = ctx. workqueue . read ( ) . await ;
316+ let total_assigned: u64 = members
317+ . iter ( )
318+ . map ( |member| workqueue. assigned_pr_count ( member. github_id ) )
319+ . sum ( ) ;
320+
321+ let table_header = |title : & str | {
322+ format ! (
323+ r"### {title}
324+ | Username | Name | Assigned PRs | Review capacity |
325+ |----------|------|-------------:|----------------:|"
326+ )
327+ } ;
328+
329+ let format_member_row = |member : & TeamMember | {
330+ let review_prefs = review_prefs. get ( member. github . as_str ( ) ) ;
331+ let max_capacity = review_prefs
332+ . as_ref ( )
333+ . and_then ( |prefs| prefs. max_assigned_prs ) ;
334+ let assigned_prs = workqueue. assigned_pr_count ( member. github_id ) ;
335+
336+ let max_capacity = max_capacity
337+ . map ( |c| c. to_string ( ) )
338+ . unwrap_or_else ( || "unlimited" . to_string ( ) ) ;
339+ format ! (
340+ "| `{}` | {} | `{assigned_prs}` | `{max_capacity}` |" ,
341+ member. github, member. name
342+ )
343+ } ;
344+
345+ let ( mut on_rotation, mut off_rotation) : ( Vec < & TeamMember > , Vec < & TeamMember > ) =
346+ members. iter ( ) . partition ( |member| {
347+ let rotation_mode = review_prefs
348+ . get ( member. github . as_str ( ) )
349+ . map ( |prefs| prefs. rotation_mode )
350+ . unwrap_or_default ( ) ;
351+ matches ! ( rotation_mode, RotationMode :: OnRotation )
352+ } ) ;
353+ on_rotation. sort_by_key ( |member| Reverse ( workqueue. assigned_pr_count ( member. github_id ) ) ) ;
354+ off_rotation. sort_by_key ( |member| Reverse ( workqueue. assigned_pr_count ( member. github_id ) ) ) ;
355+
356+ let on_rotation = on_rotation
357+ . into_iter ( )
358+ . map ( format_member_row)
359+ . collect :: < Vec < _ > > ( ) ;
360+ let off_rotation = off_rotation
361+ . into_iter ( )
362+ . map ( format_member_row)
363+ . collect :: < Vec < _ > > ( ) ;
364+
365+ // e.g. 2 members, 5 PRs assigned
366+ let mut msg = format ! (
367+ "{} {}, {} {} assigned\n \n " ,
368+ members. len( ) ,
369+ pluralize( "member" , members. len( ) ) ,
370+ total_assigned,
371+ pluralize( "PR" , total_assigned as usize )
372+ ) ;
373+ if !on_rotation. is_empty ( ) {
374+ writeln ! (
375+ msg,
376+ "{}" ,
377+ table_header( & format!( "ON rotation ({})" , on_rotation. len( ) ) )
378+ ) ?;
379+ writeln ! ( msg, "{}\n " , on_rotation. join( "\n " ) ) ?;
380+ }
381+ if !off_rotation. is_empty ( ) {
382+ writeln ! (
383+ msg,
384+ "{}" ,
385+ table_header( & format!( "OFF rotation ({})" , on_rotation. len( ) ) )
386+ ) ?;
387+ writeln ! ( msg, "{}\n " , off_rotation. join( "\n " ) ) ?;
388+ }
389+
390+ Ok ( Some ( msg) )
391+ }
392+
291393/// Returns true if we should notify user who was impersonated by someone who executed this command.
292394/// More or less, the following holds: `sensitive` == `not read-only`.
293395fn is_sensitive_command ( cmd : & ChatCommand ) -> bool {
@@ -299,6 +401,7 @@ fn is_sensitive_command(cmd: &ChatCommand) -> bool {
299401 ChatCommand :: Whoami
300402 | ChatCommand :: DocsUpdate
301403 | ChatCommand :: PingGoals ( _)
404+ | ChatCommand :: TeamStats { .. }
302405 | ChatCommand :: Lookup ( _) => false ,
303406 ChatCommand :: Work ( cmd) => match cmd {
304407 WorkqueueCmd :: Show => false ,
0 commit comments