diff --git a/hive-forge/src/verbs/comment_show.rs b/hive-forge/src/verbs/comment_show.rs index ab576af3..e3e61e1e 100644 --- a/hive-forge/src/verbs/comment_show.rs +++ b/hive-forge/src/verbs/comment_show.rs @@ -6,7 +6,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{print_json, rfc3339}; +use crate::verbs::{attachment_json, attachment_line, print_json, rfc3339}; #[derive(ClapArgs)] pub struct Args { @@ -31,11 +31,17 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "updated_at": rfc3339(c.updated_at), "body": c.body, "url": c.html_url, + "attachments": attachment_json(c.assets.as_deref()), }); print_json(&trimmed) } else { let body = c.body.as_deref().unwrap_or(""); println!("{body}"); + for a in c.assets.as_deref().unwrap_or_default() { + if let Some(line) = attachment_line(a) { + println!("{line}"); + } + } Ok(()) } } diff --git a/hive-forge/src/verbs/comments.rs b/hive-forge/src/verbs/comments.rs index 585a1582..88cebb71 100644 --- a/hive-forge/src/verbs/comments.rs +++ b/hive-forge/src/verbs/comments.rs @@ -106,6 +106,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "url": c.get("html_url"), "kind": c.get("kind").and_then(Value::as_str).unwrap_or("comment"), "state": c.get("state"), + "attachments": attachment_json_from_value(c), }) }) .collect(); @@ -131,6 +132,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> { } else { println!("**{user} @ {ts}**: {body}"); } + for line in attachment_lines_from_value(c) { + println!("{line}"); + } println!(); } if since_clamped { @@ -170,6 +174,42 @@ fn truncation_note(more_before: usize, more_after: usize) -> Option { } } +/// Attachment display lines for a single comment's already-serialized +/// `assets` field — the shape this module's merge/render pipeline works +/// on (comments arrive as `Value`s, not the typed `Comment` struct, once +/// merged with synthesized review entries). Review entries carry no +/// `assets` field at all, so they naturally yield nothing here. Mirrors +/// [`crate::verbs::attachment_line`]'s `[file: ] ` format. +fn attachment_lines_from_value(c: &Value) -> Vec { + c.get("assets") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|a| { + let name = a.get("name").and_then(Value::as_str).unwrap_or("?"); + let url = a.get("browser_download_url").and_then(Value::as_str)?; + Some(format!("[file: {name}] {url}")) + }) + .collect() +} + +/// JSON form of [`attachment_lines_from_value`]'s data, for `--json` +/// output — same `{"name", "url"}` shape [`crate::verbs::attachment_json`] +/// produces from the typed struct. +fn attachment_json_from_value(c: &Value) -> Vec { + c.get("assets") + .and_then(Value::as_array) + .into_iter() + .flatten() + .map(|a| { + json!({ + "name": a.get("name"), + "url": a.get("browser_download_url"), + }) + }) + .collect() +} + /// Total comment count on an issue/PR, straight off the issue object. /// Used both to plan `--tail`'s pagination and to report how many /// comments sit outside whatever window ends up shown. diff --git a/hive-forge/src/verbs/issue.rs b/hive-forge/src/verbs/issue.rs index 08a00049..2f31796c 100644 --- a/hive-forge/src/verbs/issue.rs +++ b/hive-forge/src/verbs/issue.rs @@ -5,7 +5,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{dependency_summaries, print_json}; +use crate::verbs::{attachment_json, dependency_summaries, print_json}; #[derive(ClapArgs)] pub struct Args { @@ -43,6 +43,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "labels": labels, "dependencies": dependencies, "body": issue.body, + "attachments": attachment_json(issue.assets.as_deref()), }); print_json(&trimmed) } diff --git a/hive-forge/src/verbs/mod.rs b/hive-forge/src/verbs/mod.rs index 38050701..a3a92f26 100644 --- a/hive-forge/src/verbs/mod.rs +++ b/hive-forge/src/verbs/mod.rs @@ -46,6 +46,7 @@ pub mod view; use std::fmt::Write as _; use anyhow::Result; +use forgejo_api::structs::Attachment; use serde_json::{Value, json}; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; @@ -375,6 +376,38 @@ pub(crate) fn dependency_summaries( .collect()) } +/// A single attachment as one display line — `[file: ] `, +/// mirroring the `[file: ...]` marker convention `read_room` already +/// uses for matrix attachments. `None` when the attachment has no +/// download URL (shouldn't happen server-side, but a missing pointer +/// is worse silently dropped than shown as "?"). +/// +/// Forgejo already returns `assets` inline on the same `Comment`/`Issue` +/// fetch every render path here already makes — this just reads a field +/// that was sitting unused, the gap that made an attachment link +/// unreadable from a non-visual CLI read without guessing the UUID by +/// hand (hit in practice on the swarm-controller extraction thread). +pub(crate) fn attachment_line(a: &Attachment) -> Option { + let name = a.name.as_deref().unwrap_or("?"); + let url = a.browser_download_url.as_ref()?; + Some(format!("[file: {name}] {url}")) +} + +/// JSON form of an attachment list (`{"name", "url"}` per entry), for +/// `--json` output — same data [`attachment_line`] renders as text. +pub(crate) fn attachment_json(assets: Option<&[Attachment]>) -> Vec { + assets + .unwrap_or_default() + .iter() + .map(|a| { + json!({ + "name": a.name, + "url": a.browser_download_url.as_ref().map(ToString::to_string), + }) + }) + .collect() +} + #[cfg(test)] mod tests { use super::{pct_encode, reviewed_older_head}; diff --git a/hive-forge/src/verbs/view.rs b/hive-forge/src/verbs/view.rs index 9b1b1b80..5f46da1d 100644 --- a/hive-forge/src/verbs/view.rs +++ b/hive-forge/src/verbs/view.rs @@ -7,6 +7,7 @@ use forgejo_api::structs::{IssueGetCommentsQuery, StateType}; use crate::client::{Client, index}; use crate::notify; +use crate::verbs::attachment_line; #[derive(ClapArgs)] pub struct Args { @@ -47,6 +48,11 @@ pub fn run(client: &Client, args: Args) -> Result<()> { println!(); println!("{body}"); } + for a in issue.assets.as_deref().unwrap_or_default() { + if let Some(line) = attachment_line(a) { + println!("{line}"); + } + } let (_, comments) = client .api() .issue_get_comments( @@ -70,6 +76,11 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .unwrap_or("?"); let cb = c.body.as_deref().unwrap_or(""); println!("**{cu}**: {cb}"); + for a in c.assets.as_deref().unwrap_or_default() { + if let Some(line) = attachment_line(a) { + println!("{line}"); + } + } println!(); } }