hive-forge: surface comment/issue attachments in comments/view/comment-show/issue
This commit is contained in:
parent
ecf9ff4d80
commit
6f3ac755e0
5 changed files with 93 additions and 2 deletions
|
|
@ -6,7 +6,7 @@ use clap::Args as ClapArgs;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::client::{Client, index};
|
use crate::client::{Client, index};
|
||||||
use crate::verbs::{print_json, rfc3339};
|
use crate::verbs::{attachment_json, attachment_line, print_json, rfc3339};
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|
@ -31,11 +31,17 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
"updated_at": rfc3339(c.updated_at),
|
"updated_at": rfc3339(c.updated_at),
|
||||||
"body": c.body,
|
"body": c.body,
|
||||||
"url": c.html_url,
|
"url": c.html_url,
|
||||||
|
"attachments": attachment_json(c.assets.as_deref()),
|
||||||
});
|
});
|
||||||
print_json(&trimmed)
|
print_json(&trimmed)
|
||||||
} else {
|
} else {
|
||||||
let body = c.body.as_deref().unwrap_or("");
|
let body = c.body.as_deref().unwrap_or("");
|
||||||
println!("{body}");
|
println!("{body}");
|
||||||
|
for a in c.assets.as_deref().unwrap_or_default() {
|
||||||
|
if let Some(line) = attachment_line(a) {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
"url": c.get("html_url"),
|
"url": c.get("html_url"),
|
||||||
"kind": c.get("kind").and_then(Value::as_str).unwrap_or("comment"),
|
"kind": c.get("kind").and_then(Value::as_str).unwrap_or("comment"),
|
||||||
"state": c.get("state"),
|
"state": c.get("state"),
|
||||||
|
"attachments": attachment_json_from_value(c),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -131,6 +132,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
println!("**{user} @ {ts}**: {body}");
|
println!("**{user} @ {ts}**: {body}");
|
||||||
}
|
}
|
||||||
|
for line in attachment_lines_from_value(c) {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
if since_clamped {
|
if since_clamped {
|
||||||
|
|
@ -170,6 +174,42 @@ fn truncation_note(more_before: usize, more_after: usize) -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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: <name>] <url>` format.
|
||||||
|
fn attachment_lines_from_value(c: &Value) -> Vec<String> {
|
||||||
|
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<Value> {
|
||||||
|
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.
|
/// Total comment count on an issue/PR, straight off the issue object.
|
||||||
/// Used both to plan `--tail`'s pagination and to report how many
|
/// Used both to plan `--tail`'s pagination and to report how many
|
||||||
/// comments sit outside whatever window ends up shown.
|
/// comments sit outside whatever window ends up shown.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use clap::Args as ClapArgs;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::client::{Client, index};
|
use crate::client::{Client, index};
|
||||||
use crate::verbs::{dependency_summaries, print_json};
|
use crate::verbs::{attachment_json, dependency_summaries, print_json};
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|
@ -43,6 +43,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
"labels": labels,
|
"labels": labels,
|
||||||
"dependencies": dependencies,
|
"dependencies": dependencies,
|
||||||
"body": issue.body,
|
"body": issue.body,
|
||||||
|
"attachments": attachment_json(issue.assets.as_deref()),
|
||||||
});
|
});
|
||||||
print_json(&trimmed)
|
print_json(&trimmed)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ pub mod view;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use forgejo_api::structs::Attachment;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
@ -375,6 +376,38 @@ pub(crate) fn dependency_summaries(
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single attachment as one display line — `[file: <name>] <url>`,
|
||||||
|
/// 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<String> {
|
||||||
|
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<Value> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{pct_encode, reviewed_older_head};
|
use super::{pct_encode, reviewed_older_head};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use forgejo_api::structs::{IssueGetCommentsQuery, StateType};
|
||||||
|
|
||||||
use crate::client::{Client, index};
|
use crate::client::{Client, index};
|
||||||
use crate::notify;
|
use crate::notify;
|
||||||
|
use crate::verbs::attachment_line;
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|
@ -47,6 +48,11 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
println!();
|
println!();
|
||||||
println!("{body}");
|
println!("{body}");
|
||||||
}
|
}
|
||||||
|
for a in issue.assets.as_deref().unwrap_or_default() {
|
||||||
|
if let Some(line) = attachment_line(a) {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
let (_, comments) = client
|
let (_, comments) = client
|
||||||
.api()
|
.api()
|
||||||
.issue_get_comments(
|
.issue_get_comments(
|
||||||
|
|
@ -70,6 +76,11 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
.unwrap_or("?");
|
.unwrap_or("?");
|
||||||
let cb = c.body.as_deref().unwrap_or("");
|
let cb = c.body.as_deref().unwrap_or("");
|
||||||
println!("**{cu}**: {cb}");
|
println!("**{cu}**: {cb}");
|
||||||
|
for a in c.assets.as_deref().unwrap_or_default() {
|
||||||
|
if let Some(line) = attachment_line(a) {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue