From b29c3825830ab588775d62871da690f3e0688058 Mon Sep 17 00:00:00 2001 From: teernisse Date: Thu, 12 Feb 2026 11:34:10 -0500 Subject: [PATCH] feat(bd-2g50): fill data gaps in issue detail view Add references_full, user_notes_count, merge_requests_count computed fields to show issue. Add closed_at and confidential columns via migration 023. Closes: bd-2g50 --- migrations/023_issue_detail_fields.sql | 5 ++ src/cli/commands/show.rs | 69 ++++++++++++++++++++++---- src/core/db.rs | 4 ++ 3 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 migrations/023_issue_detail_fields.sql diff --git a/migrations/023_issue_detail_fields.sql b/migrations/023_issue_detail_fields.sql new file mode 100644 index 0000000..c4701f2 --- /dev/null +++ b/migrations/023_issue_detail_fields.sql @@ -0,0 +1,5 @@ +ALTER TABLE issues ADD COLUMN closed_at TEXT; +ALTER TABLE issues ADD COLUMN confidential INTEGER NOT NULL DEFAULT 0; + +INSERT INTO schema_version (version, applied_at, description) +VALUES (23, strftime('%s', 'now') * 1000, 'Add closed_at and confidential to issues'); diff --git a/src/cli/commands/show.rs b/src/cli/commands/show.rs index 1d08b09..aacd8ac 100644 --- a/src/cli/commands/show.rs +++ b/src/cli/commands/show.rs @@ -75,12 +75,17 @@ pub struct IssueDetail { pub author_username: String, pub created_at: i64, pub updated_at: i64, + pub closed_at: Option, + pub confidential: bool, pub web_url: Option, pub project_path: String, + pub references_full: String, pub labels: Vec, pub assignees: Vec, pub due_date: Option, pub milestone: Option, + pub user_notes_count: i64, + pub merge_requests_count: usize, pub closing_merge_requests: Vec, pub discussions: Vec, pub status_name: Option, @@ -122,6 +127,9 @@ pub fn run_show_issue( let discussions = get_issue_discussions(&conn, issue.id)?; + let references_full = format!("{}#{}", issue.project_path, issue.iid); + let merge_requests_count = closing_mrs.len(); + Ok(IssueDetail { id: issue.id, iid: issue.iid, @@ -131,12 +139,17 @@ pub fn run_show_issue( author_username: issue.author_username, created_at: issue.created_at, updated_at: issue.updated_at, + closed_at: issue.closed_at, + confidential: issue.confidential, web_url: issue.web_url, project_path: issue.project_path, + references_full, labels, assignees, due_date: issue.due_date, milestone: issue.milestone_title, + user_notes_count: issue.user_notes_count, + merge_requests_count, closing_merge_requests: closing_mrs, discussions, status_name: issue.status_name, @@ -156,10 +169,13 @@ struct IssueRow { author_username: String, created_at: i64, updated_at: i64, + closed_at: Option, + confidential: bool, web_url: Option, project_path: String, due_date: Option, milestone_title: Option, + user_notes_count: i64, status_name: Option, status_category: Option, status_color: Option, @@ -173,8 +189,12 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu let project_id = resolve_project(conn, project)?; ( "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username, - i.created_at, i.updated_at, i.web_url, p.path_with_namespace, + i.created_at, i.updated_at, i.closed_at, i.confidential, + i.web_url, p.path_with_namespace, i.due_date, i.milestone_title, + (SELECT COUNT(*) FROM notes n + JOIN discussions d ON n.discussion_id = d.id + WHERE d.noteable_type = 'Issue' AND d.noteable_id = i.id AND n.is_system = 0) AS user_notes_count, i.status_name, i.status_category, i.status_color, i.status_icon_name, i.status_synced_at FROM issues i @@ -185,8 +205,12 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu } None => ( "SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username, - i.created_at, i.updated_at, i.web_url, p.path_with_namespace, + i.created_at, i.updated_at, i.closed_at, i.confidential, + i.web_url, p.path_with_namespace, i.due_date, i.milestone_title, + (SELECT COUNT(*) FROM notes n + JOIN discussions d ON n.discussion_id = d.id + WHERE d.noteable_type = 'Issue' AND d.noteable_id = i.id AND n.is_system = 0) AS user_notes_count, i.status_name, i.status_category, i.status_color, i.status_icon_name, i.status_synced_at FROM issues i @@ -201,6 +225,7 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu let mut stmt = conn.prepare(sql)?; let issues: Vec = stmt .query_map(param_refs.as_slice(), |row| { + let confidential_val: i64 = row.get(9)?; Ok(IssueRow { id: row.get(0)?, iid: row.get(1)?, @@ -210,15 +235,18 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu author_username: row.get(5)?, created_at: row.get(6)?, updated_at: row.get(7)?, - web_url: row.get(8)?, - project_path: row.get(9)?, - due_date: row.get(10)?, - milestone_title: row.get(11)?, - status_name: row.get(12)?, - status_category: row.get(13)?, - status_color: row.get(14)?, - status_icon_name: row.get(15)?, - status_synced_at: row.get(16)?, + closed_at: row.get(8)?, + confidential: confidential_val != 0, + web_url: row.get(10)?, + project_path: row.get(11)?, + due_date: row.get(12)?, + milestone_title: row.get(13)?, + user_notes_count: row.get(14)?, + status_name: row.get(15)?, + status_category: row.get(16)?, + status_color: row.get(17)?, + status_icon_name: row.get(18)?, + status_synced_at: row.get(19)?, }) })? .collect::, _>>()?; @@ -618,6 +646,7 @@ pub fn print_show_issue(issue: &IssueDetail) { println!("{}", "━".repeat(header.len().min(80))); println!(); + println!("Ref: {}", style(&issue.references_full).dim()); println!("Project: {}", style(&issue.project_path).cyan()); let state_styled = if issue.state == "opened" { @@ -627,6 +656,10 @@ pub fn print_show_issue(issue: &IssueDetail) { }; println!("State: {}", state_styled); + if issue.confidential { + println!(" {}", style("CONFIDENTIAL").red().bold()); + } + if let Some(status) = &issue.status_name { println!( "Status: {}", @@ -658,6 +691,10 @@ pub fn print_show_issue(issue: &IssueDetail) { println!("Created: {}", format_date(issue.created_at)); println!("Updated: {}", format_date(issue.updated_at)); + if let Some(closed_at) = &issue.closed_at { + println!("Closed: {}", closed_at); + } + if let Some(due) = &issue.due_date { println!("Due: {}", due); } @@ -931,12 +968,17 @@ pub struct IssueDetailJson { pub author_username: String, pub created_at: String, pub updated_at: String, + pub closed_at: Option, + pub confidential: bool, pub web_url: Option, pub project_path: String, + pub references_full: String, pub labels: Vec, pub assignees: Vec, pub due_date: Option, pub milestone: Option, + pub user_notes_count: i64, + pub merge_requests_count: usize, pub closing_merge_requests: Vec, pub discussions: Vec, pub status_name: Option, @@ -980,12 +1022,17 @@ impl From<&IssueDetail> for IssueDetailJson { author_username: issue.author_username.clone(), created_at: ms_to_iso(issue.created_at), updated_at: ms_to_iso(issue.updated_at), + closed_at: issue.closed_at.clone(), + confidential: issue.confidential, web_url: issue.web_url.clone(), project_path: issue.project_path.clone(), + references_full: issue.references_full.clone(), labels: issue.labels.clone(), assignees: issue.assignees.clone(), due_date: issue.due_date.clone(), milestone: issue.milestone.clone(), + user_notes_count: issue.user_notes_count, + merge_requests_count: issue.merge_requests_count, closing_merge_requests: issue .closing_merge_requests .iter() diff --git a/src/core/db.rs b/src/core/db.rs index 59086db..2cbc72c 100644 --- a/src/core/db.rs +++ b/src/core/db.rs @@ -69,6 +69,10 @@ const MIGRATIONS: &[(&str, &str)] = &[ "021", include_str!("../../migrations/021_work_item_status.sql"), ), + ( + "023", + include_str!("../../migrations/023_issue_detail_fields.sql"), + ), ]; pub fn create_connection(db_path: &Path) -> Result {