68da52b8
Show closing commit and linked patches in issue detail
a73x 2026-03-21 08:05
Issue detail view (CLI and TUI) now shows: - The commit OID that closed the issue - Backlinks to patches that reference the issue via --fixes - Patch status (open/closed/merged) in the linked patches section Adds closed_by field to IssueState, populated from the close event's commit OID. Reopen clears the field. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/state.rs b/src/state.rs index 6445b04..e7823a0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -25,6 +25,7 @@ pub struct IssueState { pub body: String, pub status: IssueStatus, pub close_reason: Option<String>, pub closed_by: Option<Oid>, pub labels: Vec<String>, pub assignees: Vec<String>, pub comments: Vec<Comment>, @@ -96,6 +97,7 @@ impl IssueState { body, status: IssueStatus::Open, close_reason: None, closed_by: None, labels: Vec::new(), assignees: Vec::new(), comments: Vec::new(), @@ -118,6 +120,7 @@ impl IssueState { if status_ts.as_ref().is_none_or(|ts| event.timestamp >= *ts) { s.status = IssueStatus::Closed; s.close_reason = reason; s.closed_by = Some(oid); status_ts = Some(event.timestamp.clone()); } } @@ -160,6 +163,8 @@ impl IssueState { if let Some(ref mut s) = state { if status_ts.as_ref().is_none_or(|ts| event.timestamp >= *ts) { s.status = IssueStatus::Open; s.close_reason = None; s.closed_by = None; status_ts = Some(event.timestamp.clone()); } } diff --git a/src/tui.rs b/src/tui.rs index 013a4e8..cc1bf92 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -403,7 +403,7 @@ fn render_detail(frame: &mut Frame, app: &App, area: Rect) { let visible = app.visible_issues(); let selected_idx = app.list_state.selected().unwrap_or(0); match visible.get(selected_idx) { Some(issue) => build_issue_detail(issue), Some(issue) => build_issue_detail(issue, &app.patches), None => Text::raw("No issues to display."), } } @@ -440,7 +440,7 @@ fn render_detail(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(para, area); } fn build_issue_detail(issue: &IssueState) -> Text<'static> { fn build_issue_detail(issue: &IssueState, patches: &[PatchState]) -> Text<'static> { let status = match issue.status { IssueStatus::Open => "open", IssueStatus::Closed => "closed", @@ -492,6 +492,47 @@ fn build_issue_detail(issue: &IssueState) -> Text<'static> { ])); } if let Some(ref oid) = issue.closed_by { lines.push(Line::from(vec![ Span::styled("Commit: ", Style::default().fg(Color::DarkGray)), Span::styled( format!("{:.8}", oid), Style::default().fg(Color::Cyan), ), ])); } // Show patches that reference this issue via --fixes let fixing_patches: Vec<&PatchState> = patches .iter() .filter(|p| p.fixes.as_deref() == Some(&issue.id)) .collect(); if !fixing_patches.is_empty() { lines.push(Line::raw("")); lines.push(Line::styled( "--- Linked Patches ---", Style::default() .fg(Color::Magenta) .add_modifier(Modifier::BOLD), )); for p in &fixing_patches { let status = match p.status { PatchStatus::Open => ("open", Color::Green), PatchStatus::Closed => ("closed", Color::Red), PatchStatus::Merged => ("merged", Color::Cyan), }; lines.push(Line::from(vec![ Span::styled( format!("{:.8}", p.id), Style::default().fg(Color::Yellow), ), Span::raw(" "), Span::styled(status.0, Style::default().fg(status.1)), Span::raw(format!(" {}", p.title)), ])); } } if !issue.body.is_empty() { lines.push(Line::raw("")); for l in issue.body.lines() {