//! `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 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, /// 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 { /// Dump title + body + all comments for an issue or PR. View(verbs::view::Args), /// Print key fields of an issue as JSON. Issue(verbs::issue::Args), /// Create an issue. Prints the issue URL on success. IssueCreate(verbs::issue_create::Args), /// Edit an issue's title, body, state, or milestone. IssueEdit(verbs::issue_edit::Args), /// Print key fields of a PR as JSON. Pr(verbs::pr::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. PrCommits(verbs::pr_commits::Args), /// Create a pull request. Prints the PR URL on success. PrCreate(verbs::pr_create::Args), /// Post a comment on an issue or PR. Comment(verbs::comment::Args), /// List all comments on an issue or PR. 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. Assign(verbs::assign::Args), /// Close an issue or PR. Close(verbs::close::Args), /// List, add, or remove labels on an issue or PR. Labels(verbs::labels::Args), /// PR health view: mergeable state, CI checks, requested reviewers + /// review verdicts, last-comment time (`--pr `). `--sha` is a /// CI-only fast path. Exit code is a merge-readiness verdict. 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 ` (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), /// 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. PrMerge(verbs::pr_merge::Args), /// List a PR's reviews, or submit one: `--approve` / /// `--request-changes` / `--comment` (with `-m` for the body). PrReviews(verbs::pr_reviews::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. Diff(verbs::diff::Args), /// Get or set this user's watch subscription on a repo. 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. 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 (` --run `). /// 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 [--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), } 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::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::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::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::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), } }