hive-forge: surface issues an issue/pr blocks in list/issue/pr
This commit is contained in:
parent
74dfd366b4
commit
a3f14f5126
4 changed files with 88 additions and 18 deletions
|
|
@ -5,7 +5,9 @@ use clap::Args as ClapArgs;
|
|||
use serde_json::json;
|
||||
|
||||
use crate::client::{Client, index};
|
||||
use crate::verbs::{attachment_json, dependency_summaries, issue_reactions, print_json};
|
||||
use crate::verbs::{
|
||||
attachment_json, blocking_summaries, dependency_summaries, issue_reactions, print_json,
|
||||
};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -34,6 +36,7 @@ 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,
|
||||
|
|
@ -43,6 +46,7 @@ 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()),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ use forgejo_api::structs::{
|
|||
use serde_json::Value;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::verbs::{dependency_summaries, labels, milestone, print_json};
|
||||
use crate::verbs::{blocking_summaries, 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,12 +178,18 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
return print_json(&items);
|
||||
}
|
||||
for item in items.as_array().into_iter().flatten() {
|
||||
let progress = item
|
||||
.get("number")
|
||||
.and_then(Value::as_u64)
|
||||
let number = item.get("number").and_then(Value::as_u64);
|
||||
let progress = number
|
||||
.map(|n| dependency_summaries(client, owner, name, n))
|
||||
.transpose()?;
|
||||
print_row(item, progress.as_deref().and_then(dep_progress));
|
||||
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),
|
||||
);
|
||||
}
|
||||
if let Some(msg) = trailer(
|
||||
headers.x_total_count.and_then(|t| u64::try_from(t).ok()),
|
||||
|
|
@ -243,11 +249,14 @@ fn trailer(total: Option<u64>, count: u64, page: u64, limit: u64) -> Option<Stri
|
|||
}
|
||||
|
||||
/// Render one issue/PR as a single `#NNN [author] title` line, plus a
|
||||
/// `(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)>) {
|
||||
/// `(N/M deps done)` suffix when `progress` is `Some` and a `(blocks N)`
|
||||
/// suffix when `blocking_open` is nonzero — the actionability signal the
|
||||
/// operator asked for: an issue blocking open work is worth picking over
|
||||
/// one with no unresolved followers, at a glance in `list`'s own output
|
||||
/// rather than a per-issue `show`. 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)>, blocking_open: 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
|
||||
|
|
@ -261,12 +270,23 @@ fn print_row(item: &Value, progress: Option<(usize, usize)>) {
|
|||
// (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 { " " };
|
||||
match progress {
|
||||
Some((done, total)) => {
|
||||
println!("#{number:>4} {kind} [{author}] {title} ({done}/{total} deps done)");
|
||||
}
|
||||
None => println!("#{number:>4} {kind} [{author}] {title}"),
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
/// Dependency completion progress from a `dependency_summaries` list —
|
||||
|
|
|
|||
|
|
@ -382,6 +382,50 @@ 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<Vec<Value>> {
|
||||
let blocking: NullableVec<BlockingIssue> = 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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use clap::Args as ClapArgs;
|
|||
use serde_json::json;
|
||||
|
||||
use crate::client::{Client, index};
|
||||
use crate::verbs::{dependency_summaries, issue_reactions, print_json};
|
||||
use crate::verbs::{blocking_summaries, dependency_summaries, issue_reactions, print_json};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -20,6 +20,7 @@ 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,
|
||||
|
|
@ -31,6 +32,7 @@ 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue