130 lines
4.1 KiB
Rust
130 lines
4.1 KiB
Rust
//! Per-verb subcommand modules. Each module exposes a `Args` struct
|
|
//! (clap-derived) and a `run` fn taking `(&Client, Args) -> Result<()>`.
|
|
//! Splitting one verb per module keeps each handler small and avoids
|
|
//! the bash script's monolithic `case` statement.
|
|
|
|
pub mod artifact_get;
|
|
pub mod assign;
|
|
pub mod attach;
|
|
pub mod attachment_get;
|
|
pub mod branches;
|
|
pub mod ci_log;
|
|
pub mod clone;
|
|
pub mod close;
|
|
pub mod comment;
|
|
pub mod comment_edit;
|
|
pub mod comment_show;
|
|
pub mod comments;
|
|
pub mod diff;
|
|
pub mod issue;
|
|
pub mod issue_create;
|
|
pub mod issue_edit;
|
|
pub mod labels;
|
|
pub mod lint;
|
|
pub mod list;
|
|
pub mod milestone;
|
|
pub mod pr;
|
|
pub mod pr_create;
|
|
pub mod pr_merge;
|
|
pub mod pr_reviews;
|
|
pub mod pr_status;
|
|
pub mod repo_add_collaborator;
|
|
pub mod repo_create;
|
|
pub mod subscription;
|
|
pub mod timeline;
|
|
pub mod tree_sha;
|
|
pub mod view;
|
|
|
|
use std::fmt::Write as _;
|
|
|
|
use anyhow::Result;
|
|
use serde_json::Value;
|
|
|
|
use crate::client::Client;
|
|
|
|
/// Pretty-print a `serde_json` value to stdout with a trailing newline,
|
|
/// matching the bash script's `| jq` output shape.
|
|
pub(crate) fn print_json(v: &Value) -> Result<()> {
|
|
let s = serde_json::to_string_pretty(v)?;
|
|
println!("{s}");
|
|
Ok(())
|
|
}
|
|
|
|
/// 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).
|
|
pub(crate) fn pct_encode(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
for b in s.bytes() {
|
|
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
|
|
out.push(b as char);
|
|
} else {
|
|
write!(out, "%{b:02X}").unwrap();
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Latest non-comment review verdict per reviewer on a PR, as
|
|
/// `(login, state)`. Reviews come oldest-first, so a later verdict from
|
|
/// the same user supersedes an earlier one; `COMMENT` / `PENDING`
|
|
/// reviews carry no verdict and are skipped. Shared by `pr-status` (for
|
|
/// its health view + readiness verdict) and `pr-merge` (for its
|
|
/// pre-merge changes-requested gate) so the verdict semantics stay in
|
|
/// one place.
|
|
pub(crate) fn latest_reviews(
|
|
client: &Client,
|
|
repo: &str,
|
|
pr: u64,
|
|
) -> Result<Vec<(String, String)>> {
|
|
let reviews = client.get_json_all(&format!("/repos/{repo}/pulls/{pr}/reviews"), 10)?;
|
|
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 {
|
|
continue;
|
|
};
|
|
let st = r.get("state").and_then(Value::as_str).unwrap_or("");
|
|
if st == "COMMENT" || st == "PENDING" || st.is_empty() {
|
|
continue;
|
|
}
|
|
if let Some(slot) = latest.iter_mut().find(|(l, _)| l == login) {
|
|
st.clone_into(&mut slot.1);
|
|
} else {
|
|
latest.push((login.to_owned(), st.to_owned()));
|
|
}
|
|
}
|
|
Ok(latest)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::pct_encode;
|
|
|
|
#[test]
|
|
fn pct_encode_passes_unreserved_through() {
|
|
// Usernames + plain label/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");
|
|
assert_eq!(pct_encode("pr1ma-paper-pdf"), "pr1ma-paper-pdf");
|
|
}
|
|
|
|
#[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.
|
|
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");
|
|
}
|
|
}
|