@@ -4,7 +4,9 @@ mod commands;
4
4
5
5
use crate :: db:: notifications:: add_metadata;
6
6
use 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
+ } ;
8
10
use crate :: github:: User ;
9
11
use crate :: handlers:: docs_update:: docs_update;
10
12
use crate :: handlers:: pr_tracking:: get_assigned_prs;
@@ -17,7 +19,8 @@ use crate::zulip::commands::{
17
19
parse_cli, ChatCommand , LookupCmd , PingGoalsArgs , StreamCommand , WorkqueueCmd , WorkqueueLimit ,
18
20
} ;
19
21
use 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 ;
21
24
use std:: fmt:: Write as _;
22
25
use subtle:: ConstantTimeEq ;
23
26
use tracing as log;
@@ -201,6 +204,7 @@ async fn handle_command<'a>(
201
204
ChatCommand :: Work ( cmd) => workqueue_commands ( ctx, gh_id, cmd) . await ,
202
205
ChatCommand :: PingGoals ( args) => ping_goals_cmd ( ctx, gh_id, & args) . await ,
203
206
ChatCommand :: DocsUpdate => trigger_docs_update ( message_data, & ctx. zulip ) ,
207
+ ChatCommand :: TeamStats { name } => team_status_cmd ( ctx, & name) . await ,
204
208
} ;
205
209
206
210
let output = output?;
@@ -288,6 +292,104 @@ async fn ping_goals_cmd(
288
292
}
289
293
}
290
294
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
+
291
393
/// Returns true if we should notify user who was impersonated by someone who executed this command.
292
394
/// More or less, the following holds: `sensitive` == `not read-only`.
293
395
fn is_sensitive_command ( cmd : & ChatCommand ) -> bool {
@@ -299,6 +401,7 @@ fn is_sensitive_command(cmd: &ChatCommand) -> bool {
299
401
ChatCommand :: Whoami
300
402
| ChatCommand :: DocsUpdate
301
403
| ChatCommand :: PingGoals ( _)
404
+ | ChatCommand :: TeamStats { .. }
302
405
| ChatCommand :: Lookup ( _) => false ,
303
406
ChatCommand :: Work ( cmd) => match cmd {
304
407
WorkqueueCmd :: Show => false ,
0 commit comments