hive-forge: defer repo resolution to the accessor, not construction

This commit is contained in:
damocles 2026-09-02 15:18:56 +02:00 committed by mara
commit fe7bf81d4a
14 changed files with 57 additions and 35 deletions

View file

@ -7,7 +7,7 @@
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result, anyhow, bail};
use forgejo_api::{Auth, ForgejoError};
use reqwest::blocking::{Client as HttpClient, Response};
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
@ -38,8 +38,11 @@ pub struct Client {
/// rather than silently falling back to the internal one.
forge_label: Option<String>,
/// Default repo used when a verb doesn't carry an explicit
/// `[repo]` override.
pub default_repo: String,
/// `[repo]` override. `None` when nothing resolved — resolution is
/// deferred to [`Client::repo`] rather than failing here, so a
/// repo-independent verb (`repo-search`, `repo-create`) never needs
/// one to exist.
default_repo: Option<String>,
/// Global `--json` flag — verbs that have a human-readable
/// default path branch on `client.json_mode()` to pick the
/// JSON output shape instead.
@ -55,10 +58,12 @@ impl Client {
/// [`infer_repo_from_cwd`]); the `HIVE_FORGE_REPO` env var (kept as
/// a last-resort override for callers that aren't in any checkout —
/// nothing in this hive's nix config sets it, so it's opt-in only).
/// No repo resolving from any of those is an error rather than a
/// silent default — a single hardcoded fallback repo was exactly
/// what made agents assume it was "the only repo they're allowed to
/// operate on".
/// No repo resolving from any of those is **not** an error here — a
/// single hardcoded fallback repo was exactly what made agents
/// assume it was "the only repo they're allowed to operate on", and
/// eagerly failing here made every verb inherit a requirement it
/// might not have. It's an error only when a repo-scoped verb
/// actually asks for one — see [`Client::repo`].
///
/// `json_mode` comes from the global `--json` flag — per-verb
/// output formatters key off it via `Client::json_mode`.
@ -71,15 +76,9 @@ impl Client {
forge_label: Option<String>,
) -> Result<Self> {
let (base, token) = resolve_credentials(forge_label.as_deref())?;
let Some(default_repo) = repo_override
let default_repo = repo_override
.or_else(infer_repo_from_cwd)
.or_else(|| std::env::var("HIVE_FORGE_REPO").ok())
else {
bail!(
"hive-forge: no repo specified — pass -r/--repo, run from inside a \
git checkout with an `origin` remote, or set HIVE_FORGE_REPO"
)
};
.or_else(|| std::env::var("HIVE_FORGE_REPO").ok());
let url = url::Url::parse(&base).with_context(|| format!("parse HIVE_FORGE_URL {base}"))?;
let api = forgejo_api::sync::Forgejo::new(Auth::Token(&token), url)
@ -147,15 +146,26 @@ impl Client {
/// Return the active repo. `from_env` already folded the
/// `-r/--repo` override into `default_repo`, so verbs just read
/// it as-is — no per-verb override plumbing.
#[must_use]
pub fn repo(&self) -> &str {
&self.default_repo
///
/// # Errors
///
/// Errors when nothing resolved a repo (see `from_env`'s doc). Only
/// a verb that actually calls this (directly or via
/// [`Client::owner_repo`]) can fail this way — `repo-search` and
/// `repo-create` never do, so they run with no repo at all.
pub fn repo(&self) -> Result<&str> {
self.default_repo.as_deref().ok_or_else(|| {
anyhow!(
"hive-forge: no repo specified — pass -r/--repo, run from inside a \
git checkout with an `origin` remote, or set HIVE_FORGE_REPO"
)
})
}
/// The active repo split into `(owner, name)` for the typed
/// client's per-segment path arguments.
pub fn owner_repo(&self) -> Result<(&str, &str)> {
split_repo(self.repo())
split_repo(self.repo()?)
}
/// Build the full URL for a Forgejo attachment by UUID.