diff --git a/hive-forge/src/verbs/list.rs b/hive-forge/src/verbs/list.rs index 01cd0439..fb2f35cb 100644 --- a/hive-forge/src/verbs/list.rs +++ b/hive-forge/src/verbs/list.rs @@ -15,6 +15,14 @@ //! matches it against title **and body** on this same endpoint, and a //! text match is only useful *composed* with the other filters — a //! separate verb would have to grow every one of them back. +//! +//! Each row in the pretty (non-`--json`) output also carries a +//! `(N/M deps done)` suffix when the item has dependencies and at least +//! one is still open — omitted entirely once every dependency is closed +//! or there are none, so a ready item's row looks exactly like it did +//! before this existed. Forgejo has no server-side dependency filter on +//! this endpoint, so this costs one extra request per row shown (not +//! per match — only the current page). use anyhow::Result; use clap::{Args as ClapArgs, ValueEnum}; @@ -24,7 +32,7 @@ use forgejo_api::structs::{ use serde_json::Value; use crate::client::Client; -use crate::verbs::{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` @@ -170,7 +178,12 @@ pub fn run(client: &Client, args: Args) -> Result<()> { return print_json(&items); } for item in items.as_array().into_iter().flatten() { - print_row(item); + let progress = item + .get("number") + .and_then(Value::as_u64) + .map(|n| dependency_summaries(client, owner, name, n)) + .transpose()?; + 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()), @@ -229,11 +242,12 @@ fn trailer(total: Option, count: u64, page: u64, limit: u64) -> Option) { 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 @@ -247,13 +261,63 @@ fn print_row(item: &Value) { // (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 { " " }; - println!("#{number:>4} {kind} [{author}] {title}"); + 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 — +/// `Some((done, total))` only when there's at least one dependency AND +/// at least one of them is still open. `None` for no dependencies, or +/// all of them already closed: the operator asked for the counter to +/// appear only "when there are deps that are not done", so a ready +/// item's row stays exactly as clean as before this existed. +fn dep_progress(deps: &[Value]) -> Option<(usize, usize)> { + let total = deps.len(); + if total == 0 { + return None; + } + let done = deps + .iter() + .filter(|d| d.get("state").and_then(Value::as_str) == Some("closed")) + .count(); + (done < total).then_some((done, total)) } #[cfg(test)] mod tests { use super::*; + fn dep(state: &str) -> Value { + serde_json::json!({ "number": 1, "title": "x", "state": state }) + } + + #[test] + fn dep_progress_none_when_no_dependencies() { + assert_eq!(dep_progress(&[]), None); + } + + #[test] + fn dep_progress_none_when_all_dependencies_closed() { + let deps = [dep("closed"), dep("closed")]; + assert_eq!(dep_progress(&deps), None); + } + + #[test] + fn dep_progress_some_when_at_least_one_dependency_still_open() { + let deps = [dep("closed"), dep("open"), dep("closed")]; + assert_eq!(dep_progress(&deps), Some((2, 3))); + } + + #[test] + fn dep_progress_counts_all_open_as_zero_done() { + let deps = [dep("open"), dep("open")]; + assert_eq!(dep_progress(&deps), Some((0, 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 d0ffca72..0a49f161 100644 --- a/hive-forge/src/verbs/mod.rs +++ b/hive-forge/src/verbs/mod.rs @@ -351,12 +351,15 @@ pub(crate) fn latest_reviews(client: &Client, repo: &str, pr: u64) -> Result