Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,21 @@ List bugs for a given repository

Show the report for a bug

**Usage:** `detail bugs show <BUG_ID>`
**Usage:** `detail bugs show [OPTIONS] <BUG_ID>`

###### **Arguments:**

* `<BUG_ID>` — Bug ID

###### **Options:**

* `--format <FORMAT>` — Output format

Default value: `table`

Possible values: `table`, `json`




## `detail bugs close`
Expand All @@ -188,6 +197,12 @@ Close a bug as resolved or dismissed
Possible values: `not-a-bug`, `wont-fix`, `duplicate`, `other`

* `--notes <NOTES>` — Additional notes
* `--format <FORMAT>` — Output format

Default value: `table`

Possible values: `table`, `json`




Expand Down
91 changes: 58 additions & 33 deletions src/commands/bugs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ pub enum BugCommands {
Show {
/// Bug ID
bug_id: String,

/// Output format
#[arg(long, value_enum, default_value = "table")]
format: crate::OutputFormat,
},

/// Close a bug as resolved or dismissed
Expand All @@ -148,6 +152,10 @@ pub enum BugCommands {
/// Additional notes
#[arg(long)]
notes: Option<String>,

/// Output format
#[arg(long, value_enum, default_value = "table")]
format: crate::OutputFormat,
},

/// Reopen a previously resolved or dismissed bug — flips it back to
Expand Down Expand Up @@ -259,6 +267,42 @@ fn validate_close_flags(
Ok((state, dismissal_reason, notes))
}

/// Render a single bug as the human-readable `bugs show` view.
fn render_bug_show(bug: &Bug) -> Result<()> {
let mut pairs: Vec<(&str, String)> = vec![
("ID", bug.id.to_string()),
("Title", bug.title.clone()),
("File", bug.file_path.as_deref().unwrap_or("-").to_string()),
("Created", format_datetime(bug.created_at)),
(
"Security",
bug.is_security_vulnerability
.map_or("-", |v| if v { "Yes" } else { "No" })
.to_string(),
),
];
if let Some(intro) = &bug.introduced_in {
pairs.push(("Introduced", format_introduced_in(intro)));
}
if let Some(review) = &bug.review {
pairs.push(("Close", review_state_label(&review.state).to_string()));
pairs.push(("Close Date", format_datetime(review.created_at)));
if let Some(reason) = &review.dismissal_reason {
pairs.push(("Dismissal", dismissal_reason_label(reason).to_string()));
}
if let Some(notes) = &review.notes {
pairs.push(("Notes", notes.clone()));
}
}
for issue in &bug.linked_issues {
pairs.push(("Issue", format_linked_issue(issue)));
}
SectionRenderer::new()
.key_value("", &pairs)
.markdown("", &bug.summary)
.print()
}

/// Page size used when scanning all bugs for client-side vulnerability filtering.
const BUG_PAGE_SIZE: u32 = 100;

Expand Down Expand Up @@ -372,7 +416,7 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
}
}

BugCommands::Show { bug_id } => {
BugCommands::Show { bug_id, format } => {
let bug_id: BugId = bug_id
.as_str()
.try_into()
Expand All @@ -382,45 +426,19 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
.await
.context("Failed to fetch bug details")?;

let mut pairs: Vec<(&str, String)> = vec![
("ID", bug.id.to_string()),
("Title", bug.title.clone()),
("File", bug.file_path.as_deref().unwrap_or("-").to_string()),
("Created", format_datetime(bug.created_at)),
(
"Security",
bug.is_security_vulnerability
.map_or("-", |v| if v { "Yes" } else { "No" })
.to_string(),
),
];
if let Some(intro) = &bug.introduced_in {
pairs.push(("Introduced", format_introduced_in(intro)));
}
if let Some(review) = &bug.review {
pairs.push(("Close", review_state_label(&review.state).to_string()));
pairs.push(("Close Date", format_datetime(review.created_at)));
if let Some(reason) = &review.dismissal_reason {
pairs.push(("Dismissal", dismissal_reason_label(reason).to_string()));
}
if let Some(notes) = &review.notes {
pairs.push(("Notes", notes.clone()));
}
if matches!(format, crate::OutputFormat::Json) {
Term::stdout().write_line(&serde_json::to_string_pretty(&bug)?)?;
return Ok(());
}
for issue in &bug.linked_issues {
pairs.push(("Issue", format_linked_issue(issue)));
}
SectionRenderer::new()
.key_value("", &pairs)
.markdown("", &bug.summary)
.print()
render_bug_show(&bug)
}

BugCommands::Close {
bug_id,
state,
dismissal_reason,
notes,
format,
} => {
let bug_id: BugId = bug_id
.as_str()
Expand Down Expand Up @@ -452,11 +470,18 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
None => None,
};

client
let review = client
.update_bug_close(&bug_id, state, dismissal_reason, notes.as_deref())
.await
.context("Failed to close bug")?;

if matches!(format, crate::OutputFormat::Json) {
// Emit only the BugReview JSON — the human-friendly success
// banner would corrupt the structured output.
Term::stdout().write_line(&serde_json::to_string_pretty(&review)?)?;
return Ok(());
}

Term::stdout()
.write_line(&format!(
"{}",
Expand Down
25 changes: 21 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ impl Cli {
const fn is_silent(&self) -> bool {
match &self.command {
Commands::Bugs { command } => match command {
commands::bugs::BugCommands::List { format, .. } => Self::is_json(format),
commands::bugs::BugCommands::Show { .. }
| commands::bugs::BugCommands::Close { .. }
| commands::bugs::BugCommands::Reopen { .. } => false,
commands::bugs::BugCommands::List { format, .. }
| commands::bugs::BugCommands::Show { format, .. }
| commands::bugs::BugCommands::Close { format, .. } => Self::is_json(format),
commands::bugs::BugCommands::Reopen { .. } => false,
},
Commands::Repos { command } => match command {
commands::repos::RepoCommands::List { format, .. } => Self::is_json(format),
Expand Down Expand Up @@ -264,6 +264,13 @@ mod tests {
assert!(!cli.is_silent());
}

#[test]
fn silent_when_bugs_show_json() {
let cli =
Cli::try_parse_from(["detail", "bugs", "show", "bug_123", "--format", "json"]).unwrap();
assert!(cli.is_silent());
}

#[test]
fn not_silent_for_bugs_close() {
let cli =
Expand Down Expand Up @@ -298,10 +305,20 @@ mod tests {

#[test]
fn not_silent_for_bugs_reopen() {
// Reopen has no JSON output to corrupt, so update notices stay on.
let cli = Cli::try_parse_from(["detail", "bugs", "reopen", "bug_123"]).unwrap();
assert!(!cli.is_silent());
}

#[test]
fn silent_when_bugs_close_json() {
let cli = Cli::try_parse_from([
"detail", "bugs", "close", "bug_123", "--state", "resolved", "--format", "json",
])
.unwrap();
assert!(cli.is_silent());
}

#[test]
fn not_silent_for_auth_status() {
let cli = Cli::try_parse_from(["detail", "auth", "status"]).unwrap();
Expand Down
Loading