From 3cb3bb60554857fa9310bfb92c5efe66119b33c7 Mon Sep 17 00:00:00 2001 From: Tyler Dammann Date: Thu, 23 Jul 2026 16:37:10 -0700 Subject: [PATCH] feat: show Detail fix PR on bugs list and show views Co-Authored-By: Claude Opus 4.8 (1M context) --- openapi.json | 30 ++++++++++++++++++++++- src/api/types.rs | 58 +++++++++++++++++++++++++++++++++++++++++--- src/commands/bugs.rs | 8 ++++-- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/openapi.json b/openapi.json index 176d0af..e5ae35f 100644 --- a/openapi.json +++ b/openapi.json @@ -53,6 +53,9 @@ "nullable": true, "type": "string" }, + "fixPr": { + "$ref": "#/components/schemas/FixPr" + }, "id": { "$ref": "#/components/schemas/BugId" }, @@ -184,7 +187,8 @@ "jira", "asana", "github_issue", - "bug_fix_check" + "bug_fix_check", + "detail_fix_pr" ], "type": "string" }, @@ -209,6 +213,30 @@ }, "type": "object" }, + "FixPr": { + "properties": { + "prNumber": { + "type": "integer" + }, + "state": { + "enum": [ + "open", + "merged", + "closed" + ], + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "prNumber", + "url", + "state" + ], + "type": "object" + }, "IntroducedIn": { "properties": { "author": { diff --git a/src/api/types.rs b/src/api/types.rs index 45ad62e..762a91f 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -6,10 +6,10 @@ use crate::utils::datetime::{format_date, format_datetime}; // Re-export generated types as the public API for this crate. pub use super::generated::types::{ Bug, BugCounts, BugDismissalReason, BugId, BugReview, BugReviewId, BugReviewState, - CreatePublicBugReviewBody, CreateRuleInput, CreateRuleResponse, IntroducedIn, LinkedIssue, - LinkedIssueTracker, ListPublicBugsWorkflowRequestId, Org, OrgId, Repo, RepoId, Rule, - RuleCreationRequestId, RuleId, RuleListItem, RuleRequestResult, RuleRequestStatus, RuleStatus, - Scan, ScanInitiator, ScanType, WorkflowStatus, + CreatePublicBugReviewBody, CreateRuleInput, CreateRuleResponse, FixPr, IntroducedIn, + LinkedIssue, LinkedIssueTracker, ListPublicBugsWorkflowRequestId, Org, OrgId, Repo, RepoId, + Rule, RuleCreationRequestId, RuleId, RuleListItem, RuleRequestResult, RuleRequestStatus, + RuleStatus, Scan, ScanInitiator, ScanType, WorkflowStatus, }; // Friendlier aliases for the generated response-wrapper names. @@ -82,6 +82,15 @@ pub fn format_linked_issue(issue: &LinkedIssue) -> String { } } +/// Format the Detail-generated fix PR (number, state, and URL) for the +/// detail/show view, rendered as `# () — `. +pub fn format_fix_pr(fix_pr: &FixPr) -> String { + format!( + "#{} ({}) \u{2014} {}", + fix_pr.pr_number, fix_pr.state, fix_pr.url + ) +} + // ── clap::ValueEnum ────────────────────────────────────────────────── impl clap::ValueEnum for BugReviewState { @@ -177,6 +186,12 @@ impl Formattable for Bug { .join(", "); pairs.push(("Linked Issues", formatted)); } + if let Some(fix_pr) = &self.fix_pr { + pairs.push(( + "Fix PR", + format!("#{} ({})", fix_pr.pr_number, fix_pr.state), + )); + } (self.title.clone(), pairs) } } @@ -452,6 +467,41 @@ mod tests { ); } + // ── fix PR ─────────────────────────────────────────────────────── + + #[test] + fn bug_card_includes_fix_pr_when_present() { + let bug: Bug = serde_json::from_value(serde_json::json!({ + "id": "bug_fixed", "title": "...", "summary": "...", + "createdAt": 1, "repoId": "repo_1", "linkedIssues": [], + "fixPr": { "prNumber": 42, "url": "https://x/pull/42", "state": "merged" } + })) + .expect("valid Bug JSON"); + let (_, pairs) = bug.to_card(); + let v = pairs.iter().find(|(k, _)| *k == "Fix PR").map(|(_, v)| v); + assert_eq!(v, Some(&"#42 (merged)".to_string())); + } + + #[test] + fn bug_card_omits_fix_pr_when_absent() { + let (_, pairs) = sample_bug().to_card(); + assert!(!pairs.iter().any(|(k, _)| *k == "Fix PR")); + } + + #[test] + fn format_fix_pr_includes_number_state_and_url() { + let fix_pr: FixPr = serde_json::from_value(serde_json::json!({ + "prNumber": 42, + "url": "https://github.com/acme/repo/pull/42", + "state": "open" + })) + .expect("valid FixPr JSON"); + assert_eq!( + format_fix_pr(&fix_pr), + "#42 (open) \u{2014} https://github.com/acme/repo/pull/42" + ); + } + // ── format_introduced_in ───────────────────────────────────────── #[test] diff --git a/src/commands/bugs.rs b/src/commands/bugs.rs index e12de8b..d749145 100644 --- a/src/commands/bugs.rs +++ b/src/commands/bugs.rs @@ -7,8 +7,9 @@ use dialoguer::{Input, Select}; use crate::api::client::ApiClient; use crate::api::types::{ - dismissal_reason_label, format_introduced_in, format_linked_issue, review_state_label, Bug, - BugDismissalReason, BugId, BugReviewState, ListPublicBugsWorkflowRequestId, RepoId, + dismissal_reason_label, format_fix_pr, format_introduced_in, format_linked_issue, + review_state_label, Bug, BugDismissalReason, BugId, BugReviewState, + ListPublicBugsWorkflowRequestId, RepoId, }; use crate::output::{output_list, SectionRenderer}; use crate::utils::datetime::{format_datetime, parse_time_spec}; @@ -333,6 +334,9 @@ fn render_bug_show(bug: &Bug) -> Result<()> { for issue in &bug.linked_issues { pairs.push(("Issue", format_linked_issue(issue))); } + if let Some(fix_pr) = &bug.fix_pr { + pairs.push(("Fix PR", format_fix_pr(fix_pr))); + } SectionRenderer::new() .key_value("", &pairs) .markdown("", &bug.summary)