diff --git a/hive-forge/src/verbs/issue.rs b/hive-forge/src/verbs/issue.rs index 038a84dc..50c8a692 100644 --- a/hive-forge/src/verbs/issue.rs +++ b/hive-forge/src/verbs/issue.rs @@ -5,9 +5,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{ - attachment_json, blocking_summaries, dependency_summaries, issue_reactions, print_json, -}; +use crate::verbs::{attachment_json, dependency_summaries, issue_reactions, print_json}; #[derive(ClapArgs)] pub struct Args { @@ -36,7 +34,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .filter_map(|l| l.name.as_deref()) .collect(); let dependencies = dependency_summaries(client, owner, name, args.number)?; - let blocking = blocking_summaries(client, owner, name, args.number)?; let reactions = issue_reactions(client, owner, name, args.number)?; let trimmed = json!({ "number": issue.number, @@ -46,7 +43,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "assignees": assignees, "labels": labels, "dependencies": dependencies, - "blocking": blocking, "reactions": reactions, "body": issue.body, "attachments": attachment_json(issue.assets.as_deref()), diff --git a/hive-forge/src/verbs/list.rs b/hive-forge/src/verbs/list.rs index 44b3c95a..fb2f35cb 100644 --- a/hive-forge/src/verbs/list.rs +++ b/hive-forge/src/verbs/list.rs @@ -32,7 +32,7 @@ use forgejo_api::structs::{ use serde_json::Value; use crate::client::Client; -use crate::verbs::{blocking_summaries, dependency_summaries, labels, milestone, print_json}; +use crate::verbs::{dependency_summaries, labels, milestone, print_json}; /// What kind of items to return. Maps onto Forgejo's `type` query /// parameter: `issues` / `pulls`, or no filter at all for `both` @@ -178,18 +178,12 @@ pub fn run(client: &Client, args: Args) -> Result<()> { return print_json(&items); } for item in items.as_array().into_iter().flatten() { - let number = item.get("number").and_then(Value::as_u64); - let progress = number + let progress = item + .get("number") + .and_then(Value::as_u64) .map(|n| dependency_summaries(client, owner, name, n)) .transpose()?; - let blocks = number - .map(|n| blocking_summaries(client, owner, name, n)) - .transpose()?; - print_row( - item, - progress.as_deref().and_then(dep_progress), - blocks.as_deref().map_or(0, blocking_open_count), - ); + print_row(item, progress.as_deref().and_then(dep_progress)); } if let Some(msg) = trailer( headers.x_total_count.and_then(|t| u64::try_from(t).ok()), @@ -249,14 +243,11 @@ fn trailer(total: Option, count: u64, page: u64, limit: u64) -> Option, blocking_open: usize) { +/// `(N/M deps done)` suffix when `progress` is `Some`. +/// Defensive: missing fields drop to placeholders so a partial +/// response from a future API change still produces readable output +/// instead of panicking on `unwrap`. +fn print_row(item: &Value, progress: Option<(usize, usize)>) { let number = item.get("number").and_then(Value::as_u64).unwrap_or(0); let title = item.get("title").and_then(Value::as_str).unwrap_or(""); let author = item @@ -270,23 +261,12 @@ fn print_row(item: &Value, progress: Option<(usize, usize)>, blocking_open: usiz // (which would render every issue as a PR). let is_pr = item.get("pull_request").is_some_and(|v| !v.is_null()); let kind = if is_pr { "PR" } else { " " }; - let deps_suffix = progress.map(|(done, total)| format!(" ({done}/{total} deps done)")); - let blocks_suffix = (blocking_open > 0).then(|| format!(" (blocks {blocking_open})")); - println!( - "#{number:>4} {kind} [{author}] {title}{}{}", - deps_suffix.unwrap_or_default(), - blocks_suffix.unwrap_or_default() - ); -} - -/// Count of *open* issues `blocking` this row's issue blocks — closed -/// followers don't count toward "actionable" (see [`print_row`]'s doc -/// comment); they're already done regardless of this one's own state. -fn blocking_open_count(blocking: &[Value]) -> usize { - blocking - .iter() - .filter(|b| b.get("state").and_then(Value::as_str) == Some("open")) - .count() + match progress { + Some((done, total)) => { + println!("#{number:>4} {kind} [{author}] {title} ({done}/{total} deps done)"); + } + None => println!("#{number:>4} {kind} [{author}] {title}"), + } } /// Dependency completion progress from a `dependency_summaries` list — @@ -338,23 +318,6 @@ mod tests { assert_eq!(dep_progress(&deps), Some((0, 2))); } - #[test] - fn blocking_open_count_zero_when_none_blocking() { - assert_eq!(blocking_open_count(&[]), 0); - } - - #[test] - fn blocking_open_count_ignores_closed_ones() { - let blocking = [dep("closed"), dep("closed")]; - assert_eq!(blocking_open_count(&blocking), 0); - } - - #[test] - fn blocking_open_count_counts_only_open_ones() { - let blocking = [dep("closed"), dep("open"), dep("open")]; - assert_eq!(blocking_open_count(&blocking), 2); - } - #[test] fn kind_query_types_match_forgejo_enum() { // The forge accepts only `issues` / `pulls` for the `type` diff --git a/hive-forge/src/verbs/mod.rs b/hive-forge/src/verbs/mod.rs index 6e901211..b48e9965 100644 --- a/hive-forge/src/verbs/mod.rs +++ b/hive-forge/src/verbs/mod.rs @@ -382,50 +382,6 @@ pub(crate) fn dependency_summaries( .collect()) } -/// One entry in a `GET .../blocks` response — same `number`/`title`/ -/// `state` shape [`dependency_summaries`] maps down to, but only those -/// fields: this struct exists to be lenient (see -/// [`Client::get_api_json`]'s doc comment), not to model the full Issue -/// response body. -#[derive(Deserialize)] -struct BlockingIssue { - #[serde(default)] - number: u64, - #[serde(default)] - title: String, - #[serde(default)] - state: String, -} - -/// The issues *blocked by* `number` — the reverse of -/// [`dependency_summaries`]. Forgejo's dependency API is one-directional -/// in `forgejo-api`'s generated client (only the forward `GET -/// .../dependencies` is wrapped), but the reverse route is real — `GET -/// .../blocks`, confirmed against Forgejo/Gitea's actual API surface, just -/// not covered by the crate — so this goes through -/// [`Client::get_api_json`] instead, same escape hatch [`issue_reactions`] -/// already uses for an uncovered route. -/// -/// # Errors -/// -/// Propagates the forge API errors from listing blocking issues. -pub(crate) fn blocking_summaries( - client: &Client, - owner: &str, - name: &str, - number: u64, -) -> Result> { - let blocking: NullableVec = client.get_api_json( - &format!("/repos/{owner}/{name}/issues/{number}/blocks"), - &[], - )?; - Ok(blocking - .0 - .into_iter() - .map(|b| json!({ "number": b.number, "title": b.title, "state": b.state })) - .collect()) -} - /// Deserialize a possibly-null JSON array as an empty `Vec`. /// /// Several Forgejo list endpoints return an explicit `null` body instead of diff --git a/hive-forge/src/verbs/pr.rs b/hive-forge/src/verbs/pr.rs index 3cb90d45..0302d218 100644 --- a/hive-forge/src/verbs/pr.rs +++ b/hive-forge/src/verbs/pr.rs @@ -5,7 +5,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{blocking_summaries, dependency_summaries, issue_reactions, print_json}; +use crate::verbs::{dependency_summaries, issue_reactions, print_json}; #[derive(ClapArgs)] pub struct Args { @@ -20,7 +20,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .repo_get_pull_request(owner, name, index(args.number)?) .send()?; let dependencies = dependency_summaries(client, owner, name, args.number)?; - let blocking = blocking_summaries(client, owner, name, args.number)?; let reactions = issue_reactions(client, owner, name, args.number)?; let trimmed = json!({ "number": pull.number, @@ -32,7 +31,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "head_branch": pull.head.as_ref().and_then(|h| h.label.as_deref()), "base_branch": pull.base.as_ref().and_then(|b| b.label.as_deref()), "dependencies": dependencies, - "blocking": blocking, "reactions": reactions, }); print_json(&trimmed)