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 close 42
|
||||||
hive-forge labels 42 add feature
|
hive-forge labels 42 add feature
|
||||||
hive-forge issue-create --title "..." --body "..."
|
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 issue-edit 42 --title "new title"
|
||||||
hive-forge pr 42 # PR metadata as JSON
|
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 --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 # list reviews; inline comments included per review
|
||||||
hive-forge pr-reviews 42 --approve # submit APPROVED review
|
hive-forge pr-reviews 42 --approve # submit APPROVED review
|
||||||
hive-forge pr-reviews 42 --request-changes -m "msg" # submit REQUEST_CHANGES 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.
|
workflow — there is no single-job variant.
|
||||||
- Do NOT use raw `curl` for forge access -- the CLI handles auth,
|
- Do NOT use raw `curl` for forge access -- the CLI handles auth,
|
||||||
error checking, and output formatting.
|
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]`
|
//! `issue-create --title <t> [body sources] [--assignee <u>]
|
||||||
//! — create an issue. Prints the issue URL.
|
//! [--label <name>]... [repo]` — create an issue. Prints the issue URL.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args as ClapArgs;
|
use clap::Args as ClapArgs;
|
||||||
|
|
@ -7,6 +7,7 @@ use forgejo_api::structs::CreateIssueOption;
|
||||||
|
|
||||||
use crate::body;
|
use crate::body;
|
||||||
use crate::client::Client;
|
use crate::client::Client;
|
||||||
|
use crate::verbs::labels;
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|
@ -22,24 +23,36 @@ pub struct Args {
|
||||||
/// Initial assignee login.
|
/// Initial assignee login.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
assignee: Option<String>,
|
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
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Propagates any I/O error from the body input (`--body-file`,
|
/// Propagates any I/O error from the body input (`--body-file`,
|
||||||
/// stdin), any transport error from the Forgejo REST call (network
|
/// stdin), any transport error from the Forgejo REST call (network
|
||||||
/// unreachable, 4xx/5xx response, token missing/invalid), and any
|
/// unreachable, 4xx/5xx response, token missing/invalid, the
|
||||||
/// I/O error from writing the issue URL to stdout.
|
/// `--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<()> {
|
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 body = body::resolve(args.body.as_deref(), args.body_file.as_deref())?.unwrap_or_default();
|
||||||
let (owner, name) = client.owner_repo()?;
|
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 {
|
let payload = CreateIssueOption {
|
||||||
assignee: None,
|
assignee: None,
|
||||||
assignees: args.assignee.map(|a| vec![a]),
|
assignees: args.assignee.map(|a| vec![a]),
|
||||||
body: Some(body),
|
body: Some(body),
|
||||||
closed: None,
|
closed: None,
|
||||||
due_date: None,
|
due_date: None,
|
||||||
labels: None,
|
labels: label_ids,
|
||||||
milestone: None,
|
milestone: None,
|
||||||
r#ref: None,
|
r#ref: None,
|
||||||
title: args.title,
|
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.
|
/// 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 (owner, name) = client.owner_repo()?;
|
||||||
let (_, labels) = client
|
let (_, labels) = client
|
||||||
.api()
|
.api()
|
||||||
|
|
@ -101,7 +104,10 @@ fn repo_labels(client: &Client) -> Result<Vec<Label>> {
|
||||||
Ok(labels)
|
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()
|
names.iter().filter_map(|n| lookup_id(all, n)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Args as ClapArgs;
|
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::body;
|
||||||
use crate::client::{Client, index};
|
use crate::client::{Client, index};
|
||||||
|
use crate::verbs::labels;
|
||||||
|
|
||||||
#[derive(ClapArgs)]
|
#[derive(ClapArgs)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|
@ -66,6 +68,14 @@ pub struct Args {
|
||||||
/// meaningful with `--agit`.
|
/// meaningful with `--agit`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
topic: Option<String>,
|
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
|
/// # Errors
|
||||||
|
|
@ -91,6 +101,12 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
||||||
push_branch(remote, head)?;
|
push_branch(remote, head)?;
|
||||||
}
|
}
|
||||||
let (owner, name) = client.owner_repo()?;
|
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` /
|
// Note: Forgejo's CreatePullRequestOption has no `draft` /
|
||||||
// `allow_maintainer_edit` fields (verified against the instance's
|
// `allow_maintainer_edit` fields (verified against the instance's
|
||||||
// swagger) — the raw client used to send both and the server
|
// 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),
|
body: Some(body),
|
||||||
due_date: None,
|
due_date: None,
|
||||||
head: Some(head.to_owned()),
|
head: Some(head.to_owned()),
|
||||||
labels: None,
|
labels: label_ids,
|
||||||
milestone: None,
|
milestone: None,
|
||||||
title: Some(args.title.clone()),
|
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}");
|
println!("{url}");
|
||||||
Ok(())
|
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
|
/// Extract the PR index from a Forgejo PR URL (`…/pulls/<n>`). Returns
|
||||||
/// `None` if the last path segment isn't a number.
|
/// `None` if the last path segment isn't a number.
|
||||||
fn pr_number_from_url(url: &str) -> Option<u64> {
|
fn pr_number_from_url(url: &str) -> Option<u64> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue