refactor(hive-forge): port CLI verbs to forgejo-api
This commit is contained in:
parent
b8a3927c43
commit
4636987469
36 changed files with 1463 additions and 1153 deletions
|
|
@ -46,8 +46,10 @@ use std::fmt::Write as _;
|
|||
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::client::{Client, index};
|
||||
|
||||
/// Pretty-print a `serde_json` value to stdout with a trailing newline,
|
||||
/// matching the bash script's `| jq` output shape.
|
||||
|
|
@ -57,6 +59,15 @@ pub(crate) fn print_json(v: &Value) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Format an optional timestamp as its RFC 3339 string — the shape the
|
||||
/// raw API emitted, so output stays stable across the typed-client
|
||||
/// port. `None` (and the never-in-practice unformattable timestamp)
|
||||
/// map to `None` so callers keep their existing null/placeholder
|
||||
/// handling.
|
||||
pub(crate) fn rfc3339(ts: Option<OffsetDateTime>) -> Option<String> {
|
||||
ts.and_then(|t| t.format(&Rfc3339).ok())
|
||||
}
|
||||
|
||||
/// Issue-vs-PR kind, for the `pr <verb>` / `issue <verb>` sub-command
|
||||
/// validation.
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -72,9 +83,12 @@ pub(crate) enum Kind {
|
|||
/// PRs and marks PRs with a non-null `pull_request` field, so one GET
|
||||
/// classifies it. Errors with a "use the other command" message on mismatch.
|
||||
pub(crate) fn assert_kind(client: &Client, number: u64, expected: Kind) -> Result<()> {
|
||||
let repo = client.repo();
|
||||
let v = client.get_json(&format!("/repos/{repo}/issues/{number}"))?;
|
||||
let is_pr = v.get("pull_request").is_some_and(|p| !p.is_null());
|
||||
let (owner, name) = client.owner_repo()?;
|
||||
let issue = client
|
||||
.api()
|
||||
.issue_get_issue(owner, name, index(number)?)
|
||||
.send()?;
|
||||
let is_pr = issue.pull_request.is_some();
|
||||
match (expected, is_pr) {
|
||||
(Kind::Pr, false) => {
|
||||
anyhow::bail!(
|
||||
|
|
@ -89,12 +103,12 @@ pub(crate) fn assert_kind(client: &Client, number: u64, expected: Kind) -> Resul
|
|||
}
|
||||
|
||||
/// Minimal RFC 3986 unreserved-set percent encoder. Covers the subset of
|
||||
/// characters that show up in the values we splice into request paths —
|
||||
/// usernames, label names, artifact names — without pulling in a fresh
|
||||
/// workspace dep. Unreserved bytes (`[A-Za-z0-9-._~]`) pass through, so
|
||||
/// the common identifier case is a no-op; everything else is `%XX`-escaped.
|
||||
/// Shared by `list` (query-string filters) and `artifact-get` (the
|
||||
/// artifact-name path segment).
|
||||
/// characters that show up in the values we splice into *web-route* paths
|
||||
/// (the typed client encodes its own path segments) — artifact names —
|
||||
/// without pulling in a fresh workspace dep. Unreserved bytes
|
||||
/// (`[A-Za-z0-9-._~]`) pass through, so the common identifier case is a
|
||||
/// no-op; everything else is `%XX`-escaped. Used by `artifact-get` (the
|
||||
/// artifact-name path segment on the web download route).
|
||||
pub(crate) fn pct_encode(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
|
|
@ -119,17 +133,30 @@ pub(crate) fn latest_reviews(
|
|||
repo: &str,
|
||||
pr: u64,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
let reviews = client.get_json_all(&format!("/repos/{repo}/pulls/{pr}/reviews"), 10)?;
|
||||
let (owner, name) = crate::client::split_repo(repo)?;
|
||||
let pr = index(pr)?;
|
||||
// Paginate (50/page, 10-page runaway cap — same ceiling the raw
|
||||
// client used) so a heavily re-reviewed PR doesn't truncate.
|
||||
let mut reviews = Vec::new();
|
||||
for page in 1..=10u32 {
|
||||
let (_, batch) = client
|
||||
.api()
|
||||
.repo_list_pull_reviews(owner, name, pr)
|
||||
.page(page)
|
||||
.page_size(50)
|
||||
.send()?;
|
||||
let short = batch.len() < 50;
|
||||
reviews.extend(batch);
|
||||
if short {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut latest: Vec<(String, String)> = Vec::new();
|
||||
for r in &reviews {
|
||||
let Some(login) = r
|
||||
.get("user")
|
||||
.and_then(|u| u.get("login"))
|
||||
.and_then(Value::as_str)
|
||||
else {
|
||||
let Some(login) = r.user.as_ref().and_then(|u| u.login.as_deref()) else {
|
||||
continue;
|
||||
};
|
||||
let st = r.get("state").and_then(Value::as_str).unwrap_or("");
|
||||
let st = r.state.as_deref().unwrap_or("");
|
||||
if st == "COMMENT" || st == "PENDING" || st.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -148,8 +175,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn pct_encode_passes_unreserved_through() {
|
||||
// Usernames + plain label/artifact names round-trip verbatim —
|
||||
// no performance regression on the common case.
|
||||
// Plain artifact names round-trip verbatim — no performance
|
||||
// regression on the common case.
|
||||
assert_eq!(pct_encode("damocles"), "damocles");
|
||||
assert_eq!(pct_encode("area-ops"), "area-ops");
|
||||
assert_eq!(pct_encode("area_ops"), "area_ops");
|
||||
|
|
@ -158,9 +185,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn pct_encode_escapes_reserved() {
|
||||
// Forgejo labels can contain spaces ("good first issue" is the
|
||||
// canonical example); `&` / `/` in any spliced value must escape
|
||||
// so they can't break out of the path/query segment.
|
||||
// `&` / `/` / spaces in any spliced value must escape so they
|
||||
// can't break out of the path/query segment.
|
||||
assert_eq!(pct_encode("good first issue"), "good%20first%20issue");
|
||||
assert_eq!(pct_encode("x&y"), "x%26y");
|
||||
assert_eq!(pct_encode("a/b"), "a%2Fb");
|
||||
|
|
|
|||
Loading…
Reference in a new issue