hive-forge: add --label to issue-create and pr-create
This commit is contained in:
parent
c236c16c52
commit
abde71b0ed
4 changed files with 94 additions and 9 deletions
|
|
@ -48,9 +48,11 @@ hive-forge assign 42 damocles
|
|||
hive-forge close 42
|
||||
hive-forge labels 42 add feature
|
||||
hive-forge issue-create --title "..." --body "..."
|
||||
hive-forge issue-create --title "..." --body "..." --label area/ops --label type/bug # repeatable
|
||||
hive-forge issue-edit 42 --title "new title"
|
||||
hive-forge pr 42 # PR metadata as JSON
|
||||
hive-forge pr-create --title "..." --head my-branch --push # also `git push forge my-branch`
|
||||
hive-forge pr-create --title "..." --head my-branch --label area/ops # repeatable, same as issue-create
|
||||
hive-forge pr-reviews 42 # list reviews; inline comments included per review
|
||||
hive-forge pr-reviews 42 --approve # submit APPROVED review
|
||||
hive-forge pr-reviews 42 --request-changes -m "msg" # submit REQUEST_CHANGES review
|
||||
|
|
@ -266,3 +268,12 @@ to discover valid label names before triaging or to audit the label set.
|
|||
workflow — there is no single-job variant.
|
||||
- Do NOT use raw `curl` for forge access -- the CLI handles auth,
|
||||
error checking, and output formatting.
|
||||
- `issue-create --label <name>` / `pr-create --label <name>` are
|
||||
repeatable and take the same spelling `labels <n> add` does. Unknown
|
||||
names are silently dropped (matching `labels add`'s existing
|
||||
behavior) rather than erroring, so a typo just means the label
|
||||
doesn't land — check `hive-forge repo-labels` if one's missing. On
|
||||
`pr-create --agit`, labels are applied as a follow-up call once the
|
||||
PR number is parsed back out of the push output (the AGit push
|
||||
itself has no label field), so they're silently skipped if that
|
||||
parse fails — same fallback as the deferred multi-line body.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! `issue-create --title <t> [body sources] [--assignee <u>] [repo]`
|
||||
//! — create an issue. Prints the issue URL.
|
||||
//! `issue-create --title <t> [body sources] [--assignee <u>]
|
||||
//! [--label <name>]... [repo]` — create an issue. Prints the issue URL.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Args as ClapArgs;
|
||||
|
|
@ -7,6 +7,7 @@ use forgejo_api::structs::CreateIssueOption;
|
|||
|
||||
use crate::body;
|
||||
use crate::client::Client;
|
||||
use crate::verbs::labels;
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -22,24 +23,36 @@ pub struct Args {
|
|||
/// Initial assignee login.
|
||||
#[arg(long)]
|
||||
assignee: Option<String>,
|
||||
/// Label name to attach, repeatable (e.g. `--label area/ops --label
|
||||
/// type/bug`). Same spelling `labels add` accepts. Unknown names are
|
||||
/// silently dropped, matching `labels add`'s existing behavior.
|
||||
#[arg(long = "label")]
|
||||
labels: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Propagates any I/O error from the body input (`--body-file`,
|
||||
/// stdin), any transport error from the Forgejo REST call (network
|
||||
/// unreachable, 4xx/5xx response, token missing/invalid), and any
|
||||
/// I/O error from writing the issue URL to stdout.
|
||||
/// unreachable, 4xx/5xx response, token missing/invalid, the
|
||||
/// `--label` lookup's own list-labels call), and any I/O error from
|
||||
/// writing the issue URL to stdout.
|
||||
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 (owner, name) = client.owner_repo()?;
|
||||
let label_ids = if args.labels.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let all = labels::repo_labels(client)?;
|
||||
Some(labels::resolve_ids(&all, &args.labels))
|
||||
};
|
||||
let payload = CreateIssueOption {
|
||||
assignee: None,
|
||||
assignees: args.assignee.map(|a| vec![a]),
|
||||
body: Some(body),
|
||||
closed: None,
|
||||
due_date: None,
|
||||
labels: None,
|
||||
labels: label_ids,
|
||||
milestone: None,
|
||||
r#ref: None,
|
||||
title: args.title,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
}
|
||||
|
||||
/// First page (100) of the repo's label set, for name → id resolution.
|
||||
fn repo_labels(client: &Client) -> Result<Vec<Label>> {
|
||||
/// `pub(crate)` so other creation verbs (`issue-create`, `pr-create`) can
|
||||
/// resolve a `--label` name to the id the create-payload structs need
|
||||
/// without duplicating the lookup.
|
||||
pub(crate) fn repo_labels(client: &Client) -> Result<Vec<Label>> {
|
||||
let (owner, name) = client.owner_repo()?;
|
||||
let (_, labels) = client
|
||||
.api()
|
||||
|
|
@ -101,7 +104,10 @@ fn repo_labels(client: &Client) -> Result<Vec<Label>> {
|
|||
Ok(labels)
|
||||
}
|
||||
|
||||
fn resolve_ids(all: &[Label], names: &[String]) -> Vec<i64> {
|
||||
/// Resolve label names to ids, silently dropping any name that doesn't
|
||||
/// match a repo label (same behavior as `labels add`/`remove` above —
|
||||
/// keeps this one non-fatal-typo policy in one place).
|
||||
pub(crate) fn resolve_ids(all: &[Label], names: &[String]) -> Vec<i64> {
|
||||
names.iter().filter_map(|n| lookup_id(all, n)).collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ use std::process::{Command, Stdio};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Args as ClapArgs;
|
||||
use forgejo_api::structs::{CreatePullRequestOption, EditIssueOption};
|
||||
use forgejo_api::structs::{CreatePullRequestOption, EditIssueOption, IssueLabelsOption};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::body;
|
||||
use crate::client::{Client, index};
|
||||
use crate::verbs::labels;
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -66,6 +68,14 @@ pub struct Args {
|
|||
/// meaningful with `--agit`.
|
||||
#[arg(long)]
|
||||
topic: Option<String>,
|
||||
/// Label name to attach, repeatable (e.g. `--label area/ops --label
|
||||
/// type/bug`). Same spelling `labels add` accepts. In `--agit` mode
|
||||
/// this is applied as a follow-up call once the PR number is known
|
||||
/// (the `AGit` push itself has no label field), so it's silently
|
||||
/// skipped if the PR URL couldn't be parsed back out of the push
|
||||
/// output — same fallback as the deferred multi-line body.
|
||||
#[arg(long = "label")]
|
||||
labels: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
|
|
@ -91,6 +101,12 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
push_branch(remote, head)?;
|
||||
}
|
||||
let (owner, name) = client.owner_repo()?;
|
||||
let label_ids = if args.labels.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let all = labels::repo_labels(client)?;
|
||||
Some(labels::resolve_ids(&all, &args.labels))
|
||||
};
|
||||
// Note: Forgejo's CreatePullRequestOption has no `draft` /
|
||||
// `allow_maintainer_edit` fields (verified against the instance's
|
||||
// swagger) — the raw client used to send both and the server
|
||||
|
|
@ -102,7 +118,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
body: Some(body),
|
||||
due_date: None,
|
||||
head: Some(head.to_owned()),
|
||||
labels: None,
|
||||
labels: label_ids,
|
||||
milestone: None,
|
||||
title: Some(args.title.clone()),
|
||||
};
|
||||
|
|
@ -210,10 +226,49 @@ fn agit_create(client: &Client, args: &Args, body: &str) -> Result<()> {
|
|||
);
|
||||
}
|
||||
}
|
||||
if !args.labels.is_empty() {
|
||||
apply_agit_labels(client, &args.labels, &url)?;
|
||||
}
|
||||
println!("{url}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Follow-up label-add call for an `--agit`-created PR: the `AGit` push
|
||||
/// itself has no label field, so this mirrors the deferred-body PATCH —
|
||||
/// resolve the PR number back out of the parsed URL, then add the
|
||||
/// resolved label ids via the same `issue_add_label` call `labels add`
|
||||
/// uses. Warns (doesn't fail the whole command) if the PR number
|
||||
/// couldn't be parsed, same fallback as the deferred body.
|
||||
fn apply_agit_labels(client: &Client, names: &[String], url: &str) -> Result<()> {
|
||||
let Some(number) = pr_number_from_url(url) else {
|
||||
eprintln!(
|
||||
"warning: --label NOT applied — could not parse PR number from {url}. \
|
||||
Set it manually: hive-forge labels <pr#> add <name>..."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
let (owner, name) = client.owner_repo()?;
|
||||
let all = labels::repo_labels(client)?;
|
||||
let ids: Vec<serde_json::Value> = labels::resolve_ids(&all, names)
|
||||
.into_iter()
|
||||
.map(|id| json!(id))
|
||||
.collect();
|
||||
client
|
||||
.api()
|
||||
.issue_add_label(
|
||||
owner,
|
||||
name,
|
||||
index(number)?,
|
||||
IssueLabelsOption {
|
||||
labels: Some(ids),
|
||||
updated_at: None,
|
||||
},
|
||||
)
|
||||
.send()
|
||||
.with_context(|| format!("set labels on AGit PR #{number}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the PR index from a Forgejo PR URL (`…/pulls/<n>`). Returns
|
||||
/// `None` if the last path segment isn't a number.
|
||||
fn pr_number_from_url(url: &str) -> Option<u64> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue