hyperhive/hive-forge/src/main.rs

224 lines
11 KiB
Rust

//! `hive-forge` — typed CLI wrapper around the in-cluster Forgejo's
//! REST API. Reads credentials from the environment:
//!
//! `HIVE_FORGE_URL` — base URL, e.g. `http://localhost:3000`
//! `HIVE_FORGE_REPO` — default repo, e.g. `hyperhive/hyperhive`
//! `HYPERHIVE_STATE_DIR` — state dir; `forge-token` lives here
//!
//! Single binary with verb subcommands. Replaces the prior bash
//! script (`hive-forge-tools.nix`) so that agents and operators get
//! the same error handling, exit codes, and JSON shapes regardless
//! of how the bash mood was that day.
#![warn(missing_docs)]
// Clap-derived `Args` structs are intentionally consumed by their
// per-verb `run` handler so we can move owned String fields out
// without cloning. The pedantic lint flags every one of them.
#![allow(clippy::needless_pass_by_value)]
mod body;
mod client;
mod notify;
mod verbs;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "hive-forge",
about = "Forgejo CLI wrapper for hyperhive",
disable_help_subcommand = true
)]
struct Cli {
/// Repo override (default from `HIVE_FORGE_REPO`).
/// Applies to any verb; replaces the per-verb `[repo]` trailing
/// positional the bash helper used.
#[arg(short = 'r', long, global = true)]
repo: Option<String>,
/// Emit JSON output instead of the verb's default human-readable
/// shape, for verbs that support both. Verbs whose
/// only output is already JSON (`issue`, `pr`, etc.) ignore this
/// flag — they always print JSON regardless.
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
verb: Verb,
}
#[derive(Subcommand)]
enum Verb {
// The kind verbs below are hidden back-compat aliases of the new
// `pr <verb>` / `issue <verb>` forms (`pr-close` -> `pr close`, bare
// `close` -> `pr close` / `issue close`, etc.). They still parse but are
// dropped from `--help`; a later change removes them once usage migrates.
/// Dump title + body + all comments for an issue or PR.
#[command(hide = true)]
View(verbs::view::Args),
/// Issue-scoped commands: `issue <show|create|edit|view|comment|comments|close|reopen|labels|assign|timeline> …`.
Issue(verbs::issue_cmd::Args),
/// Create an issue. Prints the issue URL on success.
#[command(hide = true)]
IssueCreate(verbs::issue_create::Args),
/// Edit an issue's title, body, state, or milestone.
#[command(hide = true)]
IssueEdit(verbs::issue_edit::Args),
/// PR-scoped commands: `pr <show|status|create|merge|reviews|assign-reviewer|commits|diff|view|comment|comments|close|reopen|labels|assign-committer|timeline> …`.
Pr(verbs::pr_cmd::Args),
/// List a PR's commits as JSON (sha, message, author date, author).
/// Survives rebase-rewritten shas — message + author date let a
/// caller match the rows against linear `main` history.
#[command(hide = true)]
PrCommits(verbs::pr_commits::Args),
/// Create a pull request. Prints the PR URL on success.
#[command(hide = true)]
PrCreate(verbs::pr_create::Args),
/// Post a comment on an issue or PR.
#[command(hide = true)]
Comment(verbs::comment::Args),
/// List all comments on an issue or PR.
#[command(hide = true)]
Comments(verbs::comments::Args),
/// Print the body (or full JSON) of a single comment by id.
CommentShow(verbs::comment_show::Args),
/// Edit an existing comment by id.
CommentEdit(verbs::comment_edit::Args),
/// Assign or unassign a user on an issue or PR.
#[command(hide = true)]
Assign(verbs::assign::Args),
/// Close an issue or PR.
#[command(hide = true)]
Close(verbs::close::Args),
/// List, add, or remove labels on an issue or PR.
#[command(hide = true)]
Labels(verbs::labels::Args),
/// PR health view: mergeable state, CI checks, requested reviewers +
/// review verdicts, last-comment time (`--pr <n>`). `--sha` is a
/// CI-only fast path. Exit code is a merge-readiness verdict.
#[command(hide = true)]
PrStatus(verbs::pr_status::Args),
/// Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with
/// credentials auto-injected. Pairs with `pr-create --agit`.
Clone(verbs::clone::Args),
/// Create a forge repo under the current user (or `--org`). Prints
/// the repo URL. The instance disables push-to-create, so this is
/// the supported path to a new repo. Pairs with `repo-add-collaborator`.
RepoCreate(verbs::repo_create::Args),
/// Add a collaborator to the active repo (`-r`/`HIVE_FORGE_REPO`)
/// with a permission level. Companion to `repo-create`.
RepoAddCollaborator(verbs::repo_add_collaborator::Args),
/// List the active repo's full label set (project-wide), optionally
/// filtered by a name substring. Unlike `labels <number>` (which lists
/// an issue/PR's labels), this shows every label defined on the repo —
/// the valid names + descriptions for triage / labelling. `--json`
/// emits the full label objects (id, name, color, description).
RepoLabels(verbs::repo_labels::Args),
/// Search for repositories on the forge instance by keyword, topic, or
/// description. Not repo-scoped — queries the instance-wide explore
/// endpoint. `--json` emits the full repository objects.
RepoSearch(verbs::repo_search::Args),
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
Lint(verbs::lint::Args),
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
/// `--author`, `--label`, `--limit`). Pretty rows by default; pass
/// `--json` for raw JSON.
// Discovery aliases: the verb is `list`, but `issues` / `issue-list`
// are the names people reach for first (and clap's "did you mean"
// tip points at `issue` / `issue-create`, not here). Visible so they
// show in `--help`.
#[command(visible_alias = "issues", visible_alias = "issue-list")]
List(verbs::list::Args),
/// Manage milestones (list / create / close).
Milestone(verbs::milestone::Args),
/// Merge a PR (`--method merge|rebase`, default merge). Refuses unless
/// mergeable + CI not red + no changes requested (`--force` overrides).
/// Deletes the head branch unless `--keep-branch`. No squash option.
#[command(hide = true)]
PrMerge(verbs::pr_merge::Args),
/// List a PR's reviews, or submit one: `--approve` /
/// `--request-changes` / `--comment` (with `-m` for the body).
#[command(hide = true)]
PrReviews(verbs::pr_reviews::Args),
/// Request (or withdraw with `--remove`) a review from a user on a PR.
#[command(hide = true)]
PrAssignReviewer(verbs::pr_assign_reviewer::Args),
/// List branches, optionally filtered.
Branches(verbs::branches::Args),
/// Print the tree SHA at a branch or commit.
TreeSha(verbs::tree_sha::Args),
/// Print the unified diff for a PR.
#[command(hide = true)]
Diff(verbs::diff::Args),
/// Get/set this user's watch subscription on a repo, or --list all watched repos.
Subscription(verbs::subscription::Args),
/// List timeline events on an issue or PR (closes, label adds,
/// assignments, commit refs, pushes, etc.) — the audit trail
/// `view` + `comments` don't surface.
#[command(hide = true)]
Timeline(verbs::timeline::Args),
/// Upload a file as an attachment to an issue.
AttachIssue(verbs::attach::IssueArgs),
/// Upload a file as an attachment to a comment.
AttachComment(verbs::attach::CommentArgs),
/// Download an attachment by UUID or URL. Saves to a temp file and
/// prints the path (pass `-o -` to stream raw bytes to stdout).
AttachmentGet(verbs::attachment_get::Args),
/// Download a CI Actions artifact from a run (`<name> --run <n>`).
/// Forgejo serves artifacts only via the web route, not REST; the
/// caller supplies the run number + artifact name. Saves a zip
/// (or `-o -` to stream).
ArtifactGet(verbs::artifact_get::Args),
/// Print a CI Actions run's job step logs (`--run <n> [--job i]
/// [--step i]`). Uses Forgejo's web run-view streamer (no REST
/// endpoint exists); reliable for live + recently-finished runs.
CiLog(verbs::ci_log::Args),
/// Re-run CI without an empty commit: dispatches a fresh run via the
/// workflow-dispatch API. Pass one of `--pr <n>` (the PR head branch),
/// `--run <n>` (branch + workflow resolved from that run), or
/// `--branch <name>`; `--workflow <file>` defaults to `ci.yml`.
CiRerun(verbs::ci_rerun::Args),
}
fn main() -> Result<()> {
let cli = Cli::parse();
let client = client::Client::from_env(cli.repo, cli.json).context("initialize forge client")?;
match cli.verb {
Verb::View(a) => verbs::view::run(&client, a),
Verb::Issue(a) => verbs::issue_cmd::run(&client, a),
Verb::IssueCreate(a) => verbs::issue_create::run(&client, a),
Verb::IssueEdit(a) => verbs::issue_edit::run(&client, a),
Verb::Pr(a) => verbs::pr_cmd::run(&client, a),
Verb::PrCommits(a) => verbs::pr_commits::run(&client, a),
Verb::PrCreate(a) => verbs::pr_create::run(&client, a),
Verb::Comment(a) => verbs::comment::run(&client, a),
Verb::Comments(a) => verbs::comments::run(&client, a),
Verb::CommentShow(a) => verbs::comment_show::run(&client, a),
Verb::CommentEdit(a) => verbs::comment_edit::run(&client, a),
Verb::Assign(a) => verbs::assign::run(&client, a),
Verb::Close(a) => verbs::close::run(&client, a),
Verb::Labels(a) => verbs::labels::run(&client, a),
Verb::PrStatus(a) => verbs::pr_status::run(&client, a),
Verb::Clone(a) => verbs::clone::run(&client, a),
Verb::RepoCreate(a) => verbs::repo_create::run(&client, a),
Verb::RepoAddCollaborator(a) => verbs::repo_add_collaborator::run(&client, a),
Verb::RepoLabels(a) => verbs::repo_labels::run(&client, a),
Verb::RepoSearch(a) => verbs::repo_search::run(&client, a),
Verb::Lint(a) => verbs::lint::run(&client, a),
Verb::List(a) => verbs::list::run(&client, a),
Verb::Milestone(a) => verbs::milestone::run(&client, a),
Verb::PrMerge(a) => verbs::pr_merge::run(&client, a),
Verb::PrReviews(a) => verbs::pr_reviews::run(&client, a),
Verb::PrAssignReviewer(a) => verbs::pr_assign_reviewer::run(&client, a),
Verb::Branches(a) => verbs::branches::run(&client, a),
Verb::TreeSha(a) => verbs::tree_sha::run(&client, a),
Verb::Diff(a) => verbs::diff::run(&client, a),
Verb::Subscription(a) => verbs::subscription::run(&client, a),
Verb::Timeline(a) => verbs::timeline::run(&client, a),
Verb::AttachIssue(a) => verbs::attach::run_issue(&client, a),
Verb::AttachComment(a) => verbs::attach::run_comment(&client, a),
Verb::AttachmentGet(a) => verbs::attachment_get::run(&client, a),
Verb::ArtifactGet(a) => verbs::artifact_get::run(&client, a),
Verb::CiLog(a) => verbs::ci_log::run(&client, a),
Verb::CiRerun(a) => verbs::ci_rerun::run(&client, a),
}
}