feat(who): filter unresolved discussions to open entities only
Workload and active modes now exclude discussions on closed issues and merged/closed MRs by default. Adds --include-closed flag to restore the previous behavior when needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -344,7 +344,14 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
|
||||
.map(resolve_since_required)
|
||||
.transpose()?;
|
||||
let limit = usize::from(args.limit);
|
||||
let result = query_workload(&conn, username, project_id, since_ms, limit)?;
|
||||
let result = query_workload(
|
||||
&conn,
|
||||
username,
|
||||
project_id,
|
||||
since_ms,
|
||||
limit,
|
||||
args.include_closed,
|
||||
)?;
|
||||
Ok(WhoRun {
|
||||
resolved_input: WhoResolvedInput {
|
||||
mode: "workload".to_string(),
|
||||
@@ -377,7 +384,7 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
|
||||
WhoMode::Active => {
|
||||
let since_ms = resolve_since(args.since.as_deref(), "7d")?;
|
||||
let limit = usize::from(args.limit);
|
||||
let result = query_active(&conn, project_id, since_ms, limit)?;
|
||||
let result = query_active(&conn, project_id, since_ms, limit, args.include_closed)?;
|
||||
Ok(WhoRun {
|
||||
resolved_input: WhoResolvedInput {
|
||||
mode: "active".to_string(),
|
||||
@@ -1149,6 +1156,7 @@ fn query_workload(
|
||||
project_id: Option<i64>,
|
||||
since_ms: Option<i64>,
|
||||
limit: usize,
|
||||
include_closed: bool,
|
||||
) -> Result<WorkloadResult> {
|
||||
let limit_plus_one = (limit + 1) as i64;
|
||||
|
||||
@@ -1245,7 +1253,14 @@ fn query_workload(
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
// Query 4: Unresolved discussions where user participated
|
||||
let disc_sql = "SELECT d.noteable_type,
|
||||
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 ||
|
||||
@@ -1266,10 +1281,12 @@ fn query_workload(
|
||||
)
|
||||
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";
|
||||
LIMIT ?4"
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare_cached(disc_sql)?;
|
||||
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],
|
||||
@@ -1451,35 +1468,63 @@ fn query_active(
|
||||
project_id: Option<i64>,
|
||||
since_ms: i64,
|
||||
limit: usize,
|
||||
include_closed: bool,
|
||||
) -> Result<ActiveResult> {
|
||||
let limit_plus_one = (limit + 1) as i64;
|
||||
|
||||
// Total unresolved count -- two static variants
|
||||
let total_sql_global = "SELECT COUNT(*) FROM discussions d
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1";
|
||||
let total_sql_scoped = "SELECT COUNT(*) FROM discussions d
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1
|
||||
AND d.project_id = ?2";
|
||||
|
||||
let total_unresolved_in_window: u32 = match project_id {
|
||||
None => conn.query_row(total_sql_global, rusqlite::params![since_ms], |row| {
|
||||
row.get(0)
|
||||
})?,
|
||||
Some(pid) => conn.query_row(total_sql_scoped, rusqlite::params![since_ms, pid], |row| {
|
||||
row.get(0)
|
||||
})?,
|
||||
// State filter for open-entities-only (default behavior)
|
||||
let state_joins = if include_closed {
|
||||
""
|
||||
} else {
|
||||
" LEFT JOIN issues i ON d.issue_id = i.id
|
||||
LEFT JOIN merge_requests m ON d.merge_request_id = m.id"
|
||||
};
|
||||
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')"
|
||||
};
|
||||
|
||||
// Active discussions with context -- two static SQL variants
|
||||
let sql_global = "
|
||||
// Total unresolved count -- conditionally built
|
||||
let total_sql_global = format!(
|
||||
"SELECT COUNT(*) FROM discussions d
|
||||
{state_joins}
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1
|
||||
{state_filter}"
|
||||
);
|
||||
let total_sql_scoped = format!(
|
||||
"SELECT COUNT(*) FROM discussions d
|
||||
{state_joins}
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1
|
||||
AND d.project_id = ?2
|
||||
{state_filter}"
|
||||
);
|
||||
|
||||
let total_unresolved_in_window: u32 = match project_id {
|
||||
None => conn.query_row(&total_sql_global, rusqlite::params![since_ms], |row| {
|
||||
row.get(0)
|
||||
})?,
|
||||
Some(pid) => {
|
||||
conn.query_row(&total_sql_scoped, rusqlite::params![since_ms, pid], |row| {
|
||||
row.get(0)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
// Active discussions with context -- conditionally built SQL
|
||||
let sql_global = format!(
|
||||
"
|
||||
WITH picked AS (
|
||||
SELECT d.id, d.noteable_type, d.issue_id, d.merge_request_id,
|
||||
d.project_id, d.last_note_at
|
||||
FROM discussions d
|
||||
{state_joins}
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1
|
||||
{state_filter}
|
||||
ORDER BY d.last_note_at DESC
|
||||
LIMIT ?2
|
||||
),
|
||||
@@ -1520,16 +1565,20 @@ fn query_active(
|
||||
LEFT JOIN note_counts nc ON nc.discussion_id = p.id
|
||||
LEFT JOIN participants pa ON pa.discussion_id = p.id
|
||||
ORDER BY p.last_note_at DESC
|
||||
";
|
||||
"
|
||||
);
|
||||
|
||||
let sql_scoped = "
|
||||
let sql_scoped = format!(
|
||||
"
|
||||
WITH picked AS (
|
||||
SELECT d.id, d.noteable_type, d.issue_id, d.merge_request_id,
|
||||
d.project_id, d.last_note_at
|
||||
FROM discussions d
|
||||
{state_joins}
|
||||
WHERE d.resolvable = 1 AND d.resolved = 0
|
||||
AND d.last_note_at >= ?1
|
||||
AND d.project_id = ?2
|
||||
{state_filter}
|
||||
ORDER BY d.last_note_at DESC
|
||||
LIMIT ?3
|
||||
),
|
||||
@@ -1570,7 +1619,8 @@ fn query_active(
|
||||
LEFT JOIN note_counts nc ON nc.discussion_id = p.id
|
||||
LEFT JOIN participants pa ON pa.discussion_id = p.id
|
||||
ORDER BY p.last_note_at DESC
|
||||
";
|
||||
"
|
||||
);
|
||||
|
||||
// Row-mapping closure shared between both variants
|
||||
let map_row = |row: &rusqlite::Row| -> rusqlite::Result<ActiveDiscussion> {
|
||||
@@ -1613,12 +1663,12 @@ fn query_active(
|
||||
// Select variant first, then prepare exactly one statement
|
||||
let discussions: Vec<ActiveDiscussion> = match project_id {
|
||||
None => {
|
||||
let mut stmt = conn.prepare_cached(sql_global)?;
|
||||
let mut stmt = conn.prepare_cached(&sql_global)?;
|
||||
stmt.query_map(rusqlite::params![since_ms, limit_plus_one], &map_row)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
}
|
||||
Some(pid) => {
|
||||
let mut stmt = conn.prepare_cached(sql_scoped)?;
|
||||
let mut stmt = conn.prepare_cached(&sql_scoped)?;
|
||||
stmt.query_map(rusqlite::params![since_ms, pid, limit_plus_one], &map_row)?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user