hive-forge: add -f/--forge <label> selector for external forge accounts
Targets a dashboard-provisioned external forge account (FORGES tab) instead of the internal forge: resolves `forge-<label>-token` for the token and `forge-<label>.json`'s base_url for the URL, the same two files dashboard/extra_forges.rs writes, instead of HIVE_FORGE_URL/forge-token. Falls back to today's behavior when unset. Orthogonal to -r/--repo. An unknown label gives a clear error listing the labels actually found in the state dir instead of a raw file-not-found. The base_url JSON key is read via a typed sidecar struct pinned to what extra_forges.rs writes, so the read side can't silently drift from the write side.
This commit is contained in:
parent
60a253a2f6
commit
8e58793f79
2 changed files with 110 additions and 11 deletions
|
|
@ -11,7 +11,7 @@ use anyhow::{Context, Result, bail};
|
|||
use forgejo_api::{Auth, ForgejoError};
|
||||
use reqwest::blocking::{Client as HttpClient, Response};
|
||||
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Default Forgejo URL when `HIVE_FORGE_URL` is unset.
|
||||
|
|
@ -54,12 +54,18 @@ impl Client {
|
|||
/// priority over `HIVE_FORGE_REPO`; the env var is the fallback
|
||||
/// default. `json_mode` comes from the global `--json` flag —
|
||||
/// per-verb output formatters key off it via `Client::json_mode`.
|
||||
pub fn from_env(repo_override: Option<String>, json_mode: bool) -> Result<Self> {
|
||||
let base = std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| DEFAULT_URL.to_owned());
|
||||
/// `forge_label` (from the global `-f/--forge` flag) targets a
|
||||
/// dashboard-provisioned external forge account instead of the
|
||||
/// internal forge — see [`resolve_credentials`].
|
||||
pub fn from_env(
|
||||
repo_override: Option<String>,
|
||||
json_mode: bool,
|
||||
forge_label: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let (base, token) = resolve_credentials(forge_label.as_deref())?;
|
||||
let default_repo = repo_override
|
||||
.or_else(|| std::env::var("HIVE_FORGE_REPO").ok())
|
||||
.unwrap_or_else(|| DEFAULT_REPO.to_owned());
|
||||
let token = read_token().context("read forge-token")?;
|
||||
|
||||
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)
|
||||
|
|
@ -225,15 +231,93 @@ pub fn index(n: u64) -> Result<i64> {
|
|||
i64::try_from(n).with_context(|| format!("number {n} out of range"))
|
||||
}
|
||||
|
||||
/// Resolve the `(base_url, token)` pair the client authenticates with.
|
||||
/// `None` (the default) resolves the internal forge exactly as before:
|
||||
/// `HIVE_FORGE_URL` (default [`DEFAULT_URL`]) + `read_token()`.
|
||||
/// `Some(label)` (from `-f/--forge <label>`) instead resolves a
|
||||
/// dashboard-provisioned external forge account: the token comes from
|
||||
/// `${HYPERHIVE_STATE_DIR}/forge-<label>-token` and the base URL from
|
||||
/// the `base_url` key of the sibling `forge-<label>.json` sidecar —
|
||||
/// the exact same two files `dashboard/extra_forges.rs` writes, so the
|
||||
/// read side can't drift from the write side. An unknown label (either
|
||||
/// file missing) is a clear error listing the labels actually found in
|
||||
/// the state dir, not a raw file-not-found.
|
||||
fn resolve_credentials(forge_label: Option<&str>) -> Result<(String, String)> {
|
||||
let Some(label) = forge_label else {
|
||||
let base = std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| DEFAULT_URL.to_owned());
|
||||
let token = read_token().context("read forge-token")?;
|
||||
return Ok((base, token));
|
||||
};
|
||||
|
||||
let dir = state_dir();
|
||||
let token_path = dir.join(format!("forge-{label}-token"));
|
||||
let sidecar_path = dir.join(format!("forge-{label}.json"));
|
||||
|
||||
let token = std::fs::read_to_string(&token_path).map(|s| s.trim().to_owned());
|
||||
let sidecar = std::fs::read_to_string(&sidecar_path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<ForgeSidecar>(&s).ok());
|
||||
|
||||
if let (Ok(token), Some(sidecar)) = (token, sidecar) {
|
||||
return Ok((sidecar.base_url, token));
|
||||
}
|
||||
let known = provisioned_labels(&dir);
|
||||
let known = if known.is_empty() {
|
||||
"(none provisioned)".to_owned()
|
||||
} else {
|
||||
known.join(", ")
|
||||
};
|
||||
bail!("hive-forge: no such forge {label:?} — provisioned forges: {known}");
|
||||
}
|
||||
|
||||
/// Sidecar shape `dashboard/extra_forges.rs` writes alongside each
|
||||
/// `forge-<label>-token` file: just the base URL, pinned to the same
|
||||
/// `base_url` JSON key the write side uses.
|
||||
#[derive(Deserialize)]
|
||||
struct ForgeSidecar {
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
/// Scan the state dir for every `forge-<label>-token` file (mirroring
|
||||
/// `dashboard/extra_forges.rs`'s own listing logic) and return the
|
||||
/// labels found, sorted. Used to build a helpful "did you mean one of
|
||||
/// these" error when `--forge <label>` doesn't resolve. Best-effort:
|
||||
/// an unreadable state dir yields an empty list rather than erroring
|
||||
/// (the caller already has its own error to report).
|
||||
fn provisioned_labels(dir: &std::path::Path) -> Vec<String> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut labels: Vec<String> = entries
|
||||
.flatten()
|
||||
.filter(|e| e.file_type().is_ok_and(|ft| ft.is_file()))
|
||||
.filter_map(|e| {
|
||||
e.file_name()
|
||||
.to_str()?
|
||||
.strip_prefix("forge-")?
|
||||
.strip_suffix("-token")
|
||||
.filter(|label| !label.is_empty())
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.collect();
|
||||
labels.sort();
|
||||
labels
|
||||
}
|
||||
|
||||
/// The configured `HYPERHIVE_STATE_DIR`, falling back to `$PWD` when
|
||||
/// unset — matches `read_token`'s fallback for the plain internal-forge
|
||||
/// path.
|
||||
fn state_dir() -> PathBuf {
|
||||
match std::env::var("HYPERHIVE_STATE_DIR") {
|
||||
Ok(s) if !s.is_empty() => PathBuf::from(s),
|
||||
_ => PathBuf::from("."),
|
||||
}
|
||||
}
|
||||
|
||||
/// Locate and read the forge token. Falls back to `$PWD/forge-token`
|
||||
/// when `HYPERHIVE_STATE_DIR` isn't set, matching the bash helper.
|
||||
fn read_token() -> Result<String> {
|
||||
let state_dir = std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default();
|
||||
let path = if state_dir.is_empty() {
|
||||
PathBuf::from("forge-token")
|
||||
} else {
|
||||
PathBuf::from(state_dir).join("forge-token")
|
||||
};
|
||||
let path = state_dir().join("forge-token");
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("hive-forge: no forge-token at {}", path.display()))?;
|
||||
Ok(raw.trim().to_owned())
|
||||
|
|
|
|||
Loading…
Reference in a new issue