feat(hive-forge): add repo-create + repo-add-collaborator verbs
This commit is contained in:
parent
41c4682f7a
commit
3790faf318
5 changed files with 168 additions and 0 deletions
|
|
@ -206,6 +206,23 @@ impl Client {
|
||||||
decode_json(resp, &format!("PUT {url}"))
|
decode_json(resp, &format!("PUT {url}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PUT a JSON body to `<api>/<path>` for an endpoint that returns
|
||||||
|
/// `204 No Content` (empty body), so there is nothing to decode.
|
||||||
|
/// Used by `repo-add-collaborator` (Forgejo's add-collaborator PUT
|
||||||
|
/// answers 204 on success).
|
||||||
|
pub fn put_no_content<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
|
||||||
|
let url = format!("{}{}", self.api(), path);
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.put(&url)
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.context("PUT")?;
|
||||||
|
check_status(resp, &format!("PUT {url}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// DELETE `<api>/<path>`. Optional JSON body for endpoints that
|
/// DELETE `<api>/<path>`. Optional JSON body for endpoints that
|
||||||
/// need it (Forgejo's subscription unwatch uses bodyless DELETE).
|
/// need it (Forgejo's subscription unwatch uses bodyless DELETE).
|
||||||
pub fn delete(&self, path: &str, body: Option<&Value>) -> Result<()> {
|
pub fn delete(&self, path: &str, body: Option<&Value>) -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,13 @@ enum Verb {
|
||||||
/// Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with
|
/// Clone a forge repo (default `-r`/`HIVE_FORGE_REPO`) with
|
||||||
/// credentials auto-injected. Pairs with `pr-create --agit`.
|
/// credentials auto-injected. Pairs with `pr-create --agit`.
|
||||||
Clone(verbs::clone::Args),
|
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),
|
||||||
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
|
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
|
||||||
Lint(verbs::lint::Args),
|
Lint(verbs::lint::Args),
|
||||||
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
|
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
|
||||||
|
|
@ -130,6 +137,8 @@ fn main() -> Result<()> {
|
||||||
Verb::Labels(a) => verbs::labels::run(&client, a),
|
Verb::Labels(a) => verbs::labels::run(&client, a),
|
||||||
Verb::PrStatus(a) => verbs::pr_status::run(&client, a),
|
Verb::PrStatus(a) => verbs::pr_status::run(&client, a),
|
||||||
Verb::Clone(a) => verbs::clone::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::Lint(a) => verbs::lint::run(&client, a),
|
Verb::Lint(a) => verbs::lint::run(&client, a),
|
||||||
Verb::List(a) => verbs::list::run(&client, a),
|
Verb::List(a) => verbs::list::run(&client, a),
|
||||||
Verb::Milestone(a) => verbs::milestone::run(&client, a),
|
Verb::Milestone(a) => verbs::milestone::run(&client, a),
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ pub mod pr;
|
||||||
pub mod pr_create;
|
pub mod pr_create;
|
||||||
pub mod pr_reviews;
|
pub mod pr_reviews;
|
||||||
pub mod pr_status;
|
pub mod pr_status;
|
||||||
|
pub mod repo_add_collaborator;
|
||||||
|
pub mod repo_create;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod tree_sha;
|
pub mod tree_sha;
|
||||||
|
|
|
||||||
63
hive-forge/src/verbs/repo_add_collaborator.rs
Normal file
63
hive-forge/src/verbs/repo_add_collaborator.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
//! `repo-add-collaborator <user> [--permission read|write|admin] [-r <repo>]`
|
||||||
|
//! — add `<user>` as a collaborator on the active repo (from `-r` /
|
||||||
|
//! `HIVE_FORGE_REPO`). Prints a one-line confirmation.
|
||||||
|
//!
|
||||||
|
//! Wraps `PUT /api/v1/repos/{owner}/{repo}/collaborators/{collaborator}`.
|
||||||
|
//! A fresh agent-namespace repo (see `repo-create`) usually needs peers
|
||||||
|
//! added before they can collaborate; this is the companion verb.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Args as ClapArgs;
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::client::Client;
|
||||||
|
|
||||||
|
/// Collaborator permission level accepted by Forgejo.
|
||||||
|
#[derive(Clone, Copy, ValueEnum)]
|
||||||
|
pub enum Permission {
|
||||||
|
/// Pull (read-only) access.
|
||||||
|
Read,
|
||||||
|
/// Push (read/write) access.
|
||||||
|
Write,
|
||||||
|
/// Full administrative access to the repo.
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permission {
|
||||||
|
/// The wire string Forgejo's API expects.
|
||||||
|
fn as_api(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Permission::Read => "read",
|
||||||
|
Permission::Write => "write",
|
||||||
|
Permission::Admin => "admin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ClapArgs)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Collaborator's forge login to add.
|
||||||
|
user: String,
|
||||||
|
/// Permission level to grant (default: write — a freshly added
|
||||||
|
/// collaborator usually needs to push).
|
||||||
|
#[arg(long, value_enum, default_value_t = Permission::Write)]
|
||||||
|
permission: Permission,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Propagates any transport error from the Forgejo REST call (network
|
||||||
|
/// unreachable, 4xx/5xx response such as an unknown user or insufficient
|
||||||
|
/// permission on the repo, token missing/invalid) and any I/O error from
|
||||||
|
/// writing the confirmation to stdout.
|
||||||
|
pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
|
let repo = client.repo();
|
||||||
|
let perm = args.permission.as_api();
|
||||||
|
client.put_no_content(
|
||||||
|
&format!("/repos/{repo}/collaborators/{}", args.user),
|
||||||
|
&json!({ "permission": perm }),
|
||||||
|
)?;
|
||||||
|
println!("added {} to {repo} ({perm})", args.user);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
77
hive-forge/src/verbs/repo_create.rs
Normal file
77
hive-forge/src/verbs/repo_create.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
//! `repo-create <name> [--description <d>] [--private] [--default-branch <b>]
|
||||||
|
//! [--org <org>] [--auto-init]` — create a forge repository under the current
|
||||||
|
//! user (or under `--org`). Prints the new repo's web URL (or the full repo
|
||||||
|
//! object under `--json`).
|
||||||
|
//!
|
||||||
|
//! Wraps `POST /api/v1/user/repos` (or `/orgs/{org}/repos`) so agents don't
|
||||||
|
//! reach for raw API calls when the operator greenlights a new repo — the
|
||||||
|
//! instance has push-to-create disabled, so `git push` to a non-existent
|
||||||
|
//! repo 403s. Pairs with `clone` (then push your content) and
|
||||||
|
//! `repo-add-collaborator` (add peers to a fresh agent-namespace repo).
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Args as ClapArgs;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::client::Client;
|
||||||
|
use crate::verbs::print_json;
|
||||||
|
|
||||||
|
#[derive(ClapArgs)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Repository name (required). Created under the authenticated user
|
||||||
|
/// unless `--org` is given.
|
||||||
|
name: String,
|
||||||
|
/// Repository description.
|
||||||
|
#[arg(long)]
|
||||||
|
description: Option<String>,
|
||||||
|
/// Create the repo as private (default: public).
|
||||||
|
#[arg(long)]
|
||||||
|
private: bool,
|
||||||
|
/// Default branch name (e.g. `main`). Forgejo applies it to the
|
||||||
|
/// initial commit, so it only takes effect alongside `--auto-init`.
|
||||||
|
#[arg(long = "default-branch")]
|
||||||
|
default_branch: Option<String>,
|
||||||
|
/// Create under this organisation (`POST /orgs/<org>/repos`) instead
|
||||||
|
/// of the authenticated user's namespace.
|
||||||
|
#[arg(long)]
|
||||||
|
org: Option<String>,
|
||||||
|
/// Seed an initial commit (README) so the repo is non-empty and can
|
||||||
|
/// be cloned immediately. Omit to create a bare repo you push into.
|
||||||
|
#[arg(long = "auto-init")]
|
||||||
|
auto_init: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Propagates any transport error from the Forgejo REST call (network
|
||||||
|
/// unreachable, 4xx/5xx response such as a name clash or insufficient
|
||||||
|
/// permission on the target namespace, token missing/invalid) and any
|
||||||
|
/// I/O error from writing the result to stdout.
|
||||||
|
pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
|
let mut payload = json!({
|
||||||
|
"name": args.name,
|
||||||
|
"private": args.private,
|
||||||
|
"auto_init": args.auto_init,
|
||||||
|
});
|
||||||
|
if let Some(d) = args.description {
|
||||||
|
payload["description"] = json!(d);
|
||||||
|
}
|
||||||
|
if let Some(b) = args.default_branch {
|
||||||
|
payload["default_branch"] = json!(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match args.org.as_deref() {
|
||||||
|
Some(org) => format!("/orgs/{org}/repos"),
|
||||||
|
None => "/user/repos".to_owned(),
|
||||||
|
};
|
||||||
|
let resp = client.post_json(&path, &payload)?;
|
||||||
|
|
||||||
|
if client.json_mode() {
|
||||||
|
return print_json(&resp);
|
||||||
|
}
|
||||||
|
// Default human path: print the web URL, like issue-create / pr-create.
|
||||||
|
if let Some(url) = resp.get("html_url").and_then(Value::as_str) {
|
||||||
|
println!("{url}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue