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}"))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// need it (Forgejo's subscription unwatch uses bodyless DELETE).
|
||||
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
|
||||
/// 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),
|
||||
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
|
||||
Lint(verbs::lint::Args),
|
||||
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
|
||||
|
|
@ -130,6 +137,8 @@ fn main() -> Result<()> {
|
|||
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::Lint(a) => verbs::lint::run(&client, a),
|
||||
Verb::List(a) => verbs::list::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_reviews;
|
||||
pub mod pr_status;
|
||||
pub mod repo_add_collaborator;
|
||||
pub mod repo_create;
|
||||
pub mod subscription;
|
||||
pub mod timeline;
|
||||
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