diff --git a/hive-forge/src/client.rs b/hive-forge/src/client.rs index bdfc775..cdd721f 100644 --- a/hive-forge/src/client.rs +++ b/hive-forge/src/client.rs @@ -30,10 +30,14 @@ pub struct Client { impl Client { /// Build a client from the standard environment variables. - pub fn from_env() -> Result { + /// `repo_override` (from the global `-r/--repo` flag) takes + /// priority over `HIVE_FORGE_REPO`; the env var is the fallback + /// default. + pub fn from_env(repo_override: Option) -> Result { let base = std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| DEFAULT_URL.to_owned()); - let default_repo = - std::env::var("HIVE_FORGE_REPO").unwrap_or_else(|_| DEFAULT_REPO.to_owned()); + 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 mut headers = HeaderMap::new(); @@ -59,9 +63,11 @@ impl Client { format!("{}/api/v1", self.base) } - /// Pick the user-supplied repo or fall back to the default. - pub fn repo<'a>(&'a self, override_: Option<&'a str>) -> &'a str { - override_.unwrap_or(&self.default_repo) + /// 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. + pub fn repo(&self) -> &str { + &self.default_repo } /// GET `/` and decode JSON. diff --git a/hive-forge/src/main.rs b/hive-forge/src/main.rs index d177f58..62569b0 100644 --- a/hive-forge/src/main.rs +++ b/hive-forge/src/main.rs @@ -30,6 +30,11 @@ use clap::{Parser, Subcommand}; 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, #[command(subcommand)] verb: Verb, } @@ -80,7 +85,7 @@ enum Verb { fn main() -> Result<()> { let cli = Cli::parse(); - let client = client::Client::from_env().context("initialize forge client")?; + let client = client::Client::from_env(cli.repo).context("initialize forge client")?; match cli.verb { Verb::View(a) => verbs::view::run(&client, a), Verb::Issue(a) => verbs::issue::run(&client, a), diff --git a/hive-forge/src/verbs/assign.rs b/hive-forge/src/verbs/assign.rs index 11d67e7..5c9ed71 100644 --- a/hive-forge/src/verbs/assign.rs +++ b/hive-forge/src/verbs/assign.rs @@ -19,12 +19,10 @@ pub struct Args { /// Remove the user instead of adding. #[arg(long)] remove: bool, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let current = client.get_json(&format!("/repos/{repo}/issues/{}", args.number))?; let mut assignees: Vec = current .get("assignees") diff --git a/hive-forge/src/verbs/attach.rs b/hive-forge/src/verbs/attach.rs index aef7315..91e621b 100644 --- a/hive-forge/src/verbs/attach.rs +++ b/hive-forge/src/verbs/attach.rs @@ -1,6 +1,6 @@ -//! `attach-issue [repo]` and `attach-comment -//! [repo]` — upload a file as an attachment. -//! Prints the browser download URL. +//! `attach-issue ` and `attach-comment +//! ` — upload a file as an attachment. Prints the browser +//! download URL. use std::path::PathBuf; @@ -16,8 +16,6 @@ pub struct IssueArgs { number: u64, /// File path to upload. file: PathBuf, - /// Repo override. - repo: Option, } #[derive(ClapArgs)] @@ -26,8 +24,6 @@ pub struct CommentArgs { id: u64, /// File path to upload. file: PathBuf, - /// Repo override. - repo: Option, } pub fn run_issue(client: &Client, args: IssueArgs) -> Result<()> { @@ -37,7 +33,7 @@ pub fn run_issue(client: &Client, args: IssueArgs) -> Result<()> { args.file.display() ); } - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.post_multipart_file( &format!("/repos/{repo}/issues/{}/assets", args.number), &args.file, @@ -53,7 +49,7 @@ pub fn run_comment(client: &Client, args: CommentArgs) -> Result<()> { args.file.display() ); } - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.post_multipart_file( &format!("/repos/{repo}/issues/comments/{}/assets", args.id), &args.file, diff --git a/hive-forge/src/verbs/branches.rs b/hive-forge/src/verbs/branches.rs index cd31d66..287f502 100644 --- a/hive-forge/src/verbs/branches.rs +++ b/hive-forge/src/verbs/branches.rs @@ -10,12 +10,10 @@ use crate::client::Client; pub struct Args { /// Substring pattern to filter branch names. pattern: Option, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let v = client.get_json(&format!("/repos/{repo}/branches?limit=100"))?; let names: Vec<&str> = v .as_array() diff --git a/hive-forge/src/verbs/close.rs b/hive-forge/src/verbs/close.rs index 81667d8..0d01ce2 100644 --- a/hive-forge/src/verbs/close.rs +++ b/hive-forge/src/verbs/close.rs @@ -11,12 +11,10 @@ use crate::verbs::print_json; pub struct Args { /// Issue or PR number. number: u64, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.patch_json( &format!("/repos/{repo}/issues/{}", args.number), &json!({ "state": "closed" }), diff --git a/hive-forge/src/verbs/comment.rs b/hive-forge/src/verbs/comment.rs index 051b946..e2aece1 100644 --- a/hive-forge/src/verbs/comment.rs +++ b/hive-forge/src/verbs/comment.rs @@ -19,13 +19,11 @@ pub struct Args { /// Read body from a file. `-` means stdin. #[arg(long = "body-file")] body_file: Option, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { let body = body::resolve_required(args.body.as_deref(), args.body_file.as_deref(), "comment")?; - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.post_json( &format!("/repos/{repo}/issues/{}/comments", args.number), &json!({ "body": body }), diff --git a/hive-forge/src/verbs/comment_edit.rs b/hive-forge/src/verbs/comment_edit.rs index 07a2e0f..c271a39 100644 --- a/hive-forge/src/verbs/comment_edit.rs +++ b/hive-forge/src/verbs/comment_edit.rs @@ -19,8 +19,6 @@ pub struct Args { /// Read body from a file. `-` means stdin. #[arg(long = "body-file")] body_file: Option, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { @@ -29,7 +27,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { args.body_file.as_deref(), "comment-edit", )?; - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.patch_json( &format!("/repos/{repo}/issues/comments/{}", args.id), &json!({ "body": body }), diff --git a/hive-forge/src/verbs/comment_show.rs b/hive-forge/src/verbs/comment_show.rs index 11d1630..9a2e41d 100644 --- a/hive-forge/src/verbs/comment_show.rs +++ b/hive-forge/src/verbs/comment_show.rs @@ -15,12 +15,10 @@ pub struct Args { /// Print full JSON envelope instead of just the body text. #[arg(long)] json: bool, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let v = client.get_json(&format!("/repos/{repo}/issues/comments/{}", args.id))?; if args.json { let trimmed = json!({ diff --git a/hive-forge/src/verbs/diff.rs b/hive-forge/src/verbs/diff.rs index 1106ce4..aa64c5c 100644 --- a/hive-forge/src/verbs/diff.rs +++ b/hive-forge/src/verbs/diff.rs @@ -9,12 +9,10 @@ use crate::client::Client; pub struct Args { /// PR number. number: u64, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let diff = client.get_text(&format!("/repos/{repo}/pulls/{}.diff", args.number), "text/plain")?; print!("{diff}"); Ok(()) diff --git a/hive-forge/src/verbs/issue.rs b/hive-forge/src/verbs/issue.rs index a244cd3..fdca71d 100644 --- a/hive-forge/src/verbs/issue.rs +++ b/hive-forge/src/verbs/issue.rs @@ -11,12 +11,10 @@ use crate::verbs::print_json; pub struct Args { /// Issue number. number: u64, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let v = client.get_json(&format!("/repos/{repo}/issues/{}", args.number))?; let trimmed = json!({ "number": v.get("number"), diff --git a/hive-forge/src/verbs/issue_create.rs b/hive-forge/src/verbs/issue_create.rs index 3063ad9..b80fc02 100644 --- a/hive-forge/src/verbs/issue_create.rs +++ b/hive-forge/src/verbs/issue_create.rs @@ -22,13 +22,11 @@ pub struct Args { /// Initial assignee login. #[arg(long)] assignee: Option, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { let body = body::resolve(args.body.as_deref(), args.body_file.as_deref())?.unwrap_or_default(); - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let mut payload = json!({ "title": args.title, "body": body }); if let Some(a) = args.assignee { payload["assignees"] = json!([a]); diff --git a/hive-forge/src/verbs/issue_edit.rs b/hive-forge/src/verbs/issue_edit.rs index cc612d0..ac76162 100644 --- a/hive-forge/src/verbs/issue_edit.rs +++ b/hive-forge/src/verbs/issue_edit.rs @@ -44,8 +44,6 @@ pub struct Args { /// Milestone id (0 to unset). #[arg(long)] milestone: Option, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { @@ -73,7 +71,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { payload.insert("milestone".into(), Value::Number(m.into())); } - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let resp = client.patch_json( &format!("/repos/{repo}/issues/{}", args.number), &Value::Object(payload), diff --git a/hive-forge/src/verbs/labels.rs b/hive-forge/src/verbs/labels.rs index df2990b..a034675 100644 --- a/hive-forge/src/verbs/labels.rs +++ b/hive-forge/src/verbs/labels.rs @@ -33,7 +33,7 @@ enum Action { } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(None); + let repo = client.repo(); match args.action.unwrap_or(Action::List) { Action::List => { let labels = client.get_json(&format!("/repos/{repo}/issues/{}/labels", args.number))?; diff --git a/hive-forge/src/verbs/milestone.rs b/hive-forge/src/verbs/milestone.rs index 3bec98f..5ce9000 100644 --- a/hive-forge/src/verbs/milestone.rs +++ b/hive-forge/src/verbs/milestone.rs @@ -17,10 +17,7 @@ pub struct Args { #[derive(Subcommand)] enum Action { /// List open milestones as JSON. - List { - /// Repo override. - repo: Option, - }, + List, /// Create a milestone, print {id,title}. Create { /// Milestone title. @@ -32,22 +29,18 @@ enum Action { /// Due date YYYY-MM-DD. #[arg(long)] due: Option, - /// Repo override. - repo: Option, }, /// Close a milestone by id. Close { /// Milestone id. id: u64, - /// Repo override. - repo: Option, }, } pub fn run(client: &Client, args: Args) -> Result<()> { - match args.action.unwrap_or(Action::List { repo: None }) { - Action::List { repo } => { - let repo = client.repo(repo.as_deref()); + let repo = client.repo(); + match args.action.unwrap_or(Action::List) { + Action::List => { let v = client.get_json(&format!("/repos/{repo}/milestones?state=open&limit=50"))?; let trimmed: Vec = v @@ -69,13 +62,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .unwrap_or_default(); print_json(&Value::Array(trimmed)) } - Action::Create { - title, - desc, - due, - repo, - } => { - let repo = client.repo(repo.as_deref()); + Action::Create { title, desc, due } => { let mut payload = json!({ "title": title }); if let Some(d) = desc.filter(|s| !s.is_empty()) { payload["description"] = Value::String(d); @@ -89,8 +76,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "title": resp.get("title"), })) } - Action::Close { id, repo } => { - let repo = client.repo(repo.as_deref()); + Action::Close { id } => { let resp = client.patch_json( &format!("/repos/{repo}/milestones/{id}"), &json!({ "state": "closed" }), diff --git a/hive-forge/src/verbs/pr.rs b/hive-forge/src/verbs/pr.rs index ff1a4b0..a3e101a 100644 --- a/hive-forge/src/verbs/pr.rs +++ b/hive-forge/src/verbs/pr.rs @@ -11,12 +11,10 @@ use crate::verbs::print_json; pub struct Args { /// PR number. number: u64, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let v = client.get_json(&format!("/repos/{repo}/pulls/{}", args.number))?; let trimmed = json!({ "number": v.get("number"), diff --git a/hive-forge/src/verbs/pr_create.rs b/hive-forge/src/verbs/pr_create.rs index 663d2f5..2298fff 100644 --- a/hive-forge/src/verbs/pr_create.rs +++ b/hive-forge/src/verbs/pr_create.rs @@ -28,13 +28,11 @@ pub struct Args { /// Open as draft. #[arg(long)] draft: bool, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { let body = body::resolve(args.body.as_deref(), args.body_file.as_deref())?.unwrap_or_default(); - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let payload = json!({ "title": args.title, "head": args.head, diff --git a/hive-forge/src/verbs/pr_reviews.rs b/hive-forge/src/verbs/pr_reviews.rs index 9809ff8..dbf9577 100644 --- a/hive-forge/src/verbs/pr_reviews.rs +++ b/hive-forge/src/verbs/pr_reviews.rs @@ -11,12 +11,10 @@ use crate::verbs::print_json; pub struct Args { /// PR number. number: u64, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let v = client.get_json(&format!("/repos/{repo}/pulls/{}/reviews", args.number))?; let trimmed: Vec = v .as_array() diff --git a/hive-forge/src/verbs/subscription.rs b/hive-forge/src/verbs/subscription.rs index 27ed779..8fc16d6 100644 --- a/hive-forge/src/verbs/subscription.rs +++ b/hive-forge/src/verbs/subscription.rs @@ -19,12 +19,10 @@ pub struct Args { /// Unsubscribe (clear watch + ignore). #[arg(long, group = "action")] unwatch: bool, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); if args.unwatch { client.delete(&format!("/repos/{repo}/subscription"), None)?; println!("unsubscribed"); diff --git a/hive-forge/src/verbs/tree_sha.rs b/hive-forge/src/verbs/tree_sha.rs index f935f57..e2599c9 100644 --- a/hive-forge/src/verbs/tree_sha.rs +++ b/hive-forge/src/verbs/tree_sha.rs @@ -11,12 +11,10 @@ use crate::client::Client; pub struct Args { /// Branch name or commit SHA. reference: String, - /// Repo override. - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); // Try branch first; the bash helper silently treats failure as // "not a branch, use the reference as a commit sha directly". let commit_sha = match client.get_json(&format!("/repos/{repo}/branches/{}", args.reference)) { diff --git a/hive-forge/src/verbs/view.rs b/hive-forge/src/verbs/view.rs index 4053225..01494cd 100644 --- a/hive-forge/src/verbs/view.rs +++ b/hive-forge/src/verbs/view.rs @@ -11,12 +11,10 @@ use crate::client::Client; pub struct Args { /// Issue or PR number. number: u64, - /// Repo override (default from `HIVE_FORGE_REPO`). - repo: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(args.repo.as_deref()); + let repo = client.repo(); let issue = client.get_json(&format!("/repos/{repo}/issues/{}", args.number))?; let title = issue.get("title").and_then(Value::as_str).unwrap_or(""); let body = issue.get("body").and_then(Value::as_str).unwrap_or("");