refactor(who): split 2598-line who.rs into per-mode modules
Split the monolithic who.rs into a who/ directory module with 7 focused files. The 5 query modes (expert, workload, reviews, active, overlap) share no query-level code — only types and a few small helpers — making this a clean mechanical extraction. New structure: who/types.rs — all pub result structs/enums (~185 lines) who/mod.rs — dispatch, shared helpers, JSON envelope (~428 lines) who/expert.rs — query + render + json for expert mode (~839 lines) who/workload.rs — query + render + json for workload mode (~370 lines) who/reviews.rs — query + render + json for reviews mode (~214 lines) who/active.rs — query + render + json for active mode (~299 lines) who/overlap.rs — query + render + json for overlap mode (~323 lines) Token savings: an agent working on any single mode now loads ~400-960 lines instead of 2,598 (63-85% reduction). Public API unchanged — parent mod.rs re-exports are identical. Test re-exports use #[cfg(test)] use (not pub use) to avoid visibility conflicts with pub(super) items in submodules. All 79 who tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
370
src/cli/commands/who/workload.rs
Normal file
370
src/cli/commands/who/workload.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::cli::render::{self, Icons, Theme};
|
||||
use crate::core::error::Result;
|
||||
use crate::core::time::ms_to_iso;
|
||||
|
||||
use super::types::*;
|
||||
|
||||
// ─── Query: Workload Mode ───────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn query_workload(
|
||||
conn: &Connection,
|
||||
username: &str,
|
||||
project_id: Option<i64>,
|
||||
since_ms: Option<i64>,
|
||||
limit: usize,
|
||||
include_closed: bool,
|
||||
) -> Result<WorkloadResult> {
|
||||
let limit_plus_one = (limit + 1) as i64;
|
||||
|
||||
// Query 1: Open issues assigned to user
|
||||
let issues_sql = "SELECT i.iid,
|
||||
(p.path_with_namespace || '#' || i.iid) AS ref,
|
||||
i.title, p.path_with_namespace, i.updated_at
|
||||
FROM issues i
|
||||
JOIN issue_assignees ia ON ia.issue_id = i.id
|
||||
JOIN projects p ON i.project_id = p.id
|
||||
WHERE ia.username = ?1
|
||||
AND i.state = 'opened'
|
||||
AND (?2 IS NULL OR i.project_id = ?2)
|
||||
AND (?3 IS NULL OR i.updated_at >= ?3)
|
||||
ORDER BY i.updated_at DESC
|
||||
LIMIT ?4";
|
||||
|
||||
let mut stmt = conn.prepare_cached(issues_sql)?;
|
||||
let assigned_issues: Vec<WorkloadIssue> = stmt
|
||||
.query_map(
|
||||
rusqlite::params![username, project_id, since_ms, limit_plus_one],
|
||||
|row| {
|
||||
Ok(WorkloadIssue {
|
||||
iid: row.get(0)?,
|
||||
ref_: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
project_path: row.get(3)?,
|
||||
updated_at: row.get(4)?,
|
||||
})
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Query 2: Open MRs authored
|
||||
let authored_sql = "SELECT m.iid,
|
||||
(p.path_with_namespace || '!' || m.iid) AS ref,
|
||||
m.title, m.draft, p.path_with_namespace, m.updated_at
|
||||
FROM merge_requests m
|
||||
JOIN projects p ON m.project_id = p.id
|
||||
WHERE m.author_username = ?1
|
||||
AND m.state = 'opened'
|
||||
AND (?2 IS NULL OR m.project_id = ?2)
|
||||
AND (?3 IS NULL OR m.updated_at >= ?3)
|
||||
ORDER BY m.updated_at DESC
|
||||
LIMIT ?4";
|
||||
let mut stmt = conn.prepare_cached(authored_sql)?;
|
||||
let authored_mrs: Vec<WorkloadMr> = stmt
|
||||
.query_map(
|
||||
rusqlite::params![username, project_id, since_ms, limit_plus_one],
|
||||
|row| {
|
||||
Ok(WorkloadMr {
|
||||
iid: row.get(0)?,
|
||||
ref_: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
draft: row.get::<_, i32>(3)? != 0,
|
||||
project_path: row.get(4)?,
|
||||
author_username: None,
|
||||
updated_at: row.get(5)?,
|
||||
})
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Query 3: Open MRs where user is reviewer
|
||||
let reviewing_sql = "SELECT m.iid,
|
||||
(p.path_with_namespace || '!' || m.iid) AS ref,
|
||||
m.title, m.draft, p.path_with_namespace,
|
||||
m.author_username, m.updated_at
|
||||
FROM merge_requests m
|
||||
JOIN mr_reviewers r ON r.merge_request_id = m.id
|
||||
JOIN projects p ON m.project_id = p.id
|
||||
WHERE r.username = ?1
|
||||
AND m.state = 'opened'
|
||||
AND (?2 IS NULL OR m.project_id = ?2)
|
||||
AND (?3 IS NULL OR m.updated_at >= ?3)
|
||||
ORDER BY m.updated_at DESC
|
||||
LIMIT ?4";
|
||||
let mut stmt = conn.prepare_cached(reviewing_sql)?;
|
||||
let reviewing_mrs: Vec<WorkloadMr> = stmt
|
||||
.query_map(
|
||||
rusqlite::params![username, project_id, since_ms, limit_plus_one],
|
||||
|row| {
|
||||
Ok(WorkloadMr {
|
||||
iid: row.get(0)?,
|
||||
ref_: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
draft: row.get::<_, i32>(3)? != 0,
|
||||
project_path: row.get(4)?,
|
||||
author_username: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Query 4: Unresolved discussions where user participated
|
||||
let state_filter = if include_closed {
|
||||
""
|
||||
} else {
|
||||
" AND (i.id IS NULL OR i.state = 'opened')
|
||||
AND (m.id IS NULL OR m.state = 'opened')"
|
||||
};
|
||||
let disc_sql = format!(
|
||||
"SELECT d.noteable_type,
|
||||
COALESCE(i.iid, m.iid) AS entity_iid,
|
||||
(p.path_with_namespace ||
|
||||
CASE WHEN d.noteable_type = 'MergeRequest' THEN '!' ELSE '#' END ||
|
||||
COALESCE(i.iid, m.iid)) AS ref,
|
||||
COALESCE(i.title, m.title) AS entity_title,
|
||||
p.path_with_namespace,
|
||||
d.last_note_at
|
||||
FROM discussions d
|
||||
JOIN projects p ON d.project_id = p.id
|
||||
LEFT JOIN issues i ON d.issue_id = i.id
|
||||
LEFT JOIN merge_requests m ON d.merge_request_id = m.id
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM notes n
|
||||
WHERE n.discussion_id = d.id
|
||||
AND n.author_username = ?1
|
||||
AND n.is_system = 0
|
||||
)
|
||||
AND (?2 IS NULL OR d.project_id = ?2)
|
||||
AND (?3 IS NULL OR d.last_note_at >= ?3)
|
||||
{state_filter}
|
||||
ORDER BY d.last_note_at DESC
|
||||
LIMIT ?4"
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare_cached(&disc_sql)?;
|
||||
let unresolved_discussions: Vec<WorkloadDiscussion> = stmt
|
||||
.query_map(
|
||||
rusqlite::params![username, project_id, since_ms, limit_plus_one],
|
||||
|row| {
|
||||
let noteable_type: String = row.get(0)?;
|
||||
let entity_type = if noteable_type == "MergeRequest" {
|
||||
"MR"
|
||||
} else {
|
||||
"Issue"
|
||||
};
|
||||
Ok(WorkloadDiscussion {
|
||||
entity_type: entity_type.to_string(),
|
||||
entity_iid: row.get(1)?,
|
||||
ref_: row.get(2)?,
|
||||
entity_title: row.get(3)?,
|
||||
project_path: row.get(4)?,
|
||||
last_note_at: row.get(5)?,
|
||||
})
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Truncation detection
|
||||
let assigned_issues_truncated = assigned_issues.len() > limit;
|
||||
let authored_mrs_truncated = authored_mrs.len() > limit;
|
||||
let reviewing_mrs_truncated = reviewing_mrs.len() > limit;
|
||||
let unresolved_discussions_truncated = unresolved_discussions.len() > limit;
|
||||
|
||||
let assigned_issues: Vec<WorkloadIssue> = assigned_issues.into_iter().take(limit).collect();
|
||||
let authored_mrs: Vec<WorkloadMr> = authored_mrs.into_iter().take(limit).collect();
|
||||
let reviewing_mrs: Vec<WorkloadMr> = reviewing_mrs.into_iter().take(limit).collect();
|
||||
let unresolved_discussions: Vec<WorkloadDiscussion> =
|
||||
unresolved_discussions.into_iter().take(limit).collect();
|
||||
|
||||
Ok(WorkloadResult {
|
||||
username: username.to_string(),
|
||||
assigned_issues,
|
||||
authored_mrs,
|
||||
reviewing_mrs,
|
||||
unresolved_discussions,
|
||||
assigned_issues_truncated,
|
||||
authored_mrs_truncated,
|
||||
reviewing_mrs_truncated,
|
||||
unresolved_discussions_truncated,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Human Renderer: Workload ───────────────────────────────────────────────
|
||||
|
||||
pub(super) fn print_workload_human(r: &WorkloadResult) {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
Theme::bold().render(&format!(
|
||||
"{} {} -- Workload Summary",
|
||||
Icons::user(),
|
||||
r.username
|
||||
))
|
||||
);
|
||||
println!("{}", "\u{2500}".repeat(60));
|
||||
|
||||
if !r.assigned_issues.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!("Assigned Issues ({})", r.assigned_issues.len()))
|
||||
);
|
||||
for item in &r.assigned_issues {
|
||||
println!(
|
||||
" {} {} {}",
|
||||
Theme::info().render(&item.ref_),
|
||||
render::truncate(&item.title, 40),
|
||||
Theme::dim().render(&render::format_relative_time(item.updated_at)),
|
||||
);
|
||||
}
|
||||
if r.assigned_issues_truncated {
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("(truncated; rerun with a higher --limit)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !r.authored_mrs.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!("Authored MRs ({})", r.authored_mrs.len()))
|
||||
);
|
||||
for mr in &r.authored_mrs {
|
||||
let draft = if mr.draft { " [draft]" } else { "" };
|
||||
println!(
|
||||
" {} {}{} {}",
|
||||
Theme::info().render(&mr.ref_),
|
||||
render::truncate(&mr.title, 35),
|
||||
Theme::dim().render(draft),
|
||||
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
||||
);
|
||||
}
|
||||
if r.authored_mrs_truncated {
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("(truncated; rerun with a higher --limit)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !r.reviewing_mrs.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!("Reviewing MRs ({})", r.reviewing_mrs.len()))
|
||||
);
|
||||
for mr in &r.reviewing_mrs {
|
||||
let author = mr
|
||||
.author_username
|
||||
.as_deref()
|
||||
.map(|a| format!(" by @{a}"))
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
" {} {}{} {}",
|
||||
Theme::info().render(&mr.ref_),
|
||||
render::truncate(&mr.title, 30),
|
||||
Theme::dim().render(&author),
|
||||
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
||||
);
|
||||
}
|
||||
if r.reviewing_mrs_truncated {
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("(truncated; rerun with a higher --limit)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !r.unresolved_discussions.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
render::section_divider(&format!(
|
||||
"Unresolved Discussions ({})",
|
||||
r.unresolved_discussions.len()
|
||||
))
|
||||
);
|
||||
for disc in &r.unresolved_discussions {
|
||||
println!(
|
||||
" {} {} {} {}",
|
||||
Theme::dim().render(&disc.entity_type),
|
||||
Theme::info().render(&disc.ref_),
|
||||
render::truncate(&disc.entity_title, 35),
|
||||
Theme::dim().render(&render::format_relative_time(disc.last_note_at)),
|
||||
);
|
||||
}
|
||||
if r.unresolved_discussions_truncated {
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("(truncated; rerun with a higher --limit)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if r.assigned_issues.is_empty()
|
||||
&& r.authored_mrs.is_empty()
|
||||
&& r.reviewing_mrs.is_empty()
|
||||
&& r.unresolved_discussions.is_empty()
|
||||
{
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("No open work items found for this user.")
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
// ─── JSON Renderer: Workload ────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn workload_to_json(r: &WorkloadResult) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"username": r.username,
|
||||
"assigned_issues": r.assigned_issues.iter().map(|i| serde_json::json!({
|
||||
"iid": i.iid,
|
||||
"ref": i.ref_,
|
||||
"title": i.title,
|
||||
"project_path": i.project_path,
|
||||
"updated_at": ms_to_iso(i.updated_at),
|
||||
})).collect::<Vec<_>>(),
|
||||
"authored_mrs": r.authored_mrs.iter().map(|m| serde_json::json!({
|
||||
"iid": m.iid,
|
||||
"ref": m.ref_,
|
||||
"title": m.title,
|
||||
"draft": m.draft,
|
||||
"project_path": m.project_path,
|
||||
"updated_at": ms_to_iso(m.updated_at),
|
||||
})).collect::<Vec<_>>(),
|
||||
"reviewing_mrs": r.reviewing_mrs.iter().map(|m| serde_json::json!({
|
||||
"iid": m.iid,
|
||||
"ref": m.ref_,
|
||||
"title": m.title,
|
||||
"draft": m.draft,
|
||||
"project_path": m.project_path,
|
||||
"author_username": m.author_username,
|
||||
"updated_at": ms_to_iso(m.updated_at),
|
||||
})).collect::<Vec<_>>(),
|
||||
"unresolved_discussions": r.unresolved_discussions.iter().map(|d| serde_json::json!({
|
||||
"entity_type": d.entity_type,
|
||||
"entity_iid": d.entity_iid,
|
||||
"ref": d.ref_,
|
||||
"entity_title": d.entity_title,
|
||||
"project_path": d.project_path,
|
||||
"last_note_at": ms_to_iso(d.last_note_at),
|
||||
})).collect::<Vec<_>>(),
|
||||
"summary": {
|
||||
"assigned_issue_count": r.assigned_issues.len(),
|
||||
"authored_mr_count": r.authored_mrs.len(),
|
||||
"reviewing_mr_count": r.reviewing_mrs.len(),
|
||||
"unresolved_discussion_count": r.unresolved_discussions.len(),
|
||||
},
|
||||
"truncation": {
|
||||
"assigned_issues_truncated": r.assigned_issues_truncated,
|
||||
"authored_mrs_truncated": r.authored_mrs_truncated,
|
||||
"reviewing_mrs_truncated": r.reviewing_mrs_truncated,
|
||||
"unresolved_discussions_truncated": r.unresolved_discussions_truncated,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user