@@ -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 , 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;
@@ -196,6 +199,7 @@ async fn handle_command<'a>(
196199 ChatCommand :: Whoami => whoami_cmd ( & ctx, gh_id) . await ,
197200 ChatCommand :: Lookup ( cmd) => lookup_cmd ( & ctx, cmd) . await ,
198201 ChatCommand :: Work ( cmd) => workqueue_commands ( ctx, gh_id, cmd) . await ,
202+ ChatCommand :: TeamStats { name } => team_status_cmd ( ctx, & name) . await ,
199203 } ;
200204
201205 let output = output?;
@@ -281,6 +285,104 @@ async fn handle_command<'a>(
281285 }
282286}
283287
288+ async fn team_status_cmd ( ctx : & Context , team_name : & str ) -> anyhow:: Result < Option < String > > {
289+ use std:: fmt:: Write ;
290+
291+ let Some ( team) = ctx. team_api . get_team ( team_name) . await ? else {
292+ return Ok ( Some ( format ! ( "Team {team_name} not found" ) ) ) ;
293+ } ;
294+
295+ let mut members = team. members ;
296+ members. sort_by ( |a, b| a. github . cmp ( & b. github ) ) ;
297+
298+ let usernames: Vec < & str > = members
299+ . iter ( )
300+ . map ( |member| member. github . as_str ( ) )
301+ . collect ( ) ;
302+
303+ let db = ctx. db . get ( ) . await ;
304+ let review_prefs = get_review_prefs_batch ( & db, & usernames)
305+ . await
306+ . context ( "cannot load review preferences" ) ?;
307+
308+ let workqueue = ctx. workqueue . read ( ) . await ;
309+ let total_assigned: u64 = members
310+ . iter ( )
311+ . map ( |member| workqueue. assigned_pr_count ( member. github_id ) )
312+ . sum ( ) ;
313+
314+ let table_header = |title : & str | {
315+ format ! (
316+ r"### {title}
317+ | Username | Name | Assigned PRs | Review capacity |
318+ |----------|------|-------------:|----------------:|"
319+ )
320+ } ;
321+
322+ let format_member_row = |member : & TeamMember | {
323+ let review_prefs = review_prefs. get ( member. github . as_str ( ) ) ;
324+ let max_capacity = review_prefs
325+ . as_ref ( )
326+ . and_then ( |prefs| prefs. max_assigned_prs ) ;
327+ let assigned_prs = workqueue. assigned_pr_count ( member. github_id ) ;
328+
329+ let max_capacity = max_capacity
330+ . map ( |c| c. to_string ( ) )
331+ . unwrap_or_else ( || "unlimited" . to_string ( ) ) ;
332+ format ! (
333+ "| `{}` | {} | `{assigned_prs}` | `{max_capacity}` |" ,
334+ member. github, member. name
335+ )
336+ } ;
337+
338+ let ( mut on_rotation, mut off_rotation) : ( Vec < & TeamMember > , Vec < & TeamMember > ) =
339+ members. iter ( ) . partition ( |member| {
340+ let rotation_mode = review_prefs
341+ . get ( member. github . as_str ( ) )
342+ . map ( |prefs| prefs. rotation_mode )
343+ . unwrap_or_default ( ) ;
344+ matches ! ( rotation_mode, RotationMode :: OnRotation )
345+ } ) ;
346+ on_rotation. sort_by_key ( |member| Reverse ( workqueue. assigned_pr_count ( member. github_id ) ) ) ;
347+ off_rotation. sort_by_key ( |member| Reverse ( workqueue. assigned_pr_count ( member. github_id ) ) ) ;
348+
349+ let on_rotation = on_rotation
350+ . into_iter ( )
351+ . map ( format_member_row)
352+ . collect :: < Vec < _ > > ( ) ;
353+ let off_rotation = off_rotation
354+ . into_iter ( )
355+ . map ( format_member_row)
356+ . collect :: < Vec < _ > > ( ) ;
357+
358+ // e.g. 2 members, 5 PRs assigned
359+ let mut msg = format ! (
360+ "{} {}, {} {} assigned\n \n " ,
361+ members. len( ) ,
362+ pluralize( "member" , members. len( ) ) ,
363+ total_assigned,
364+ pluralize( "PR" , total_assigned as usize )
365+ ) ;
366+ if !on_rotation. is_empty ( ) {
367+ writeln ! (
368+ msg,
369+ "{}" ,
370+ table_header( & format!( "ON rotation ({})" , on_rotation. len( ) ) )
371+ ) ?;
372+ writeln ! ( msg, "{}\n " , on_rotation. join( "\n " ) ) ?;
373+ }
374+ if !off_rotation. is_empty ( ) {
375+ writeln ! (
376+ msg,
377+ "{}" ,
378+ table_header( & format!( "OFF rotation ({})" , on_rotation. len( ) ) )
379+ ) ?;
380+ writeln ! ( msg, "{}\n " , off_rotation. join( "\n " ) ) ?;
381+ }
382+
383+ Ok ( Some ( msg) )
384+ }
385+
284386/// Returns true if we should notify user who was impersonated by someone who executed this command.
285387/// More or less, the following holds: `sensitive` == `not read-only`.
286388fn is_sensitive_command ( cmd : & ChatCommand ) -> bool {
@@ -289,8 +391,7 @@ fn is_sensitive_command(cmd: &ChatCommand) -> bool {
289391 | ChatCommand :: Add { .. }
290392 | ChatCommand :: Move { .. }
291393 | ChatCommand :: Meta { .. } => true ,
292- ChatCommand :: Whoami => false ,
293- ChatCommand :: Lookup ( _) => false ,
394+ ChatCommand :: Whoami | ChatCommand :: Lookup ( _) | ChatCommand :: TeamStats { .. } => false ,
294395 ChatCommand :: Work ( cmd) => match cmd {
295396 WorkqueueCmd :: Show => false ,
296397 WorkqueueCmd :: SetPrLimit { .. } => true ,
0 commit comments