diff --git a/docs/tools/forge-cli.md b/docs/tools/forge-cli.md index 0cc936c9..d26344c3 100644 --- a/docs/tools/forge-cli.md +++ b/docs/tools/forge-cli.md @@ -532,7 +532,7 @@ Create a pull request Default value: `main` * `--body ` — Inline body text * `--body-file ` — Read body from a file. `-` means stdin -* `--draft` — Open as draft. Ignored in `--agit` mode +* `--draft` — Not supported by this forge (no draft field): warns on stderr and opens a normal PR * `--push` — Push the local `--head` branch to `--remote` before creating the PR * `--remote ` — Remote to push to @@ -1278,7 +1278,7 @@ Get/set this user's watch subscription on a repo, or --list all watched repos ###### **Options:** * `--watch` — Subscribe (watch the repo) -* `--ignore` — Mute (mark ignored) +* `--ignore` — Not supported: the forge API has no ignore endpoint. Errors; use `--unwatch` to stop notifications * `--unwatch` — Unsubscribe (clear watch + ignore) * `--list` — List every repo the current user watches (ignores `[repo]`) diff --git a/hive-forge/src/verbs/pr_create.rs b/hive-forge/src/verbs/pr_create.rs index 8885fca9..f427febc 100644 --- a/hive-forge/src/verbs/pr_create.rs +++ b/hive-forge/src/verbs/pr_create.rs @@ -50,7 +50,8 @@ pub struct Args { /// Read body from a file. `-` means stdin. #[arg(long = "body-file")] body_file: Option, - /// Open as draft. Ignored in `--agit` mode. + /// Not supported by this forge (no draft field): warns on stderr and + /// opens a normal PR. #[arg(long)] draft: bool, /// Push the local `--head` branch to `--remote` before creating the PR. @@ -88,6 +89,9 @@ pub struct Args { /// token missing/invalid), an unresolved `--label` name, and any I/O /// error from writing the PR URL to stdout. pub fn run(client: &Client, args: Args) -> Result<()> { + if args.draft { + eprintln!("{DRAFT_WARNING}"); + } let body = body::resolve(args.body.as_deref(), args.body_file.as_deref())?.unwrap_or_default(); if args.agit { return agit_create(client, &args, &body); @@ -109,10 +113,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> { let all = labels::repo_labels(client)?; Some(labels::resolve_ids(&all, &args.labels)?) }; - // Note: Forgejo's CreatePullRequestOption has no `draft` / + // 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 - // silently dropped them, so omitting them here changes nothing. + // swagger). let payload = CreatePullRequestOption { assignee: None, assignees: None, @@ -201,6 +204,9 @@ fn agit_create(client: &Client, args: &Args, body: &str) -> Result<()> { ); return Ok(()); }; + // The PR exists once the push succeeds; print the URL before any + // follow-up so a failing step cannot hide it and prompt a duplicate. + println!("{url}"); if deferred_body { if let Some(number) = pr_number_from_url(&url) { let (owner, name) = client.owner_repo()?; @@ -231,7 +237,6 @@ 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(()) } @@ -380,6 +385,8 @@ fn remote_content(line: &str) -> Option<&str> { .map(str::trim_start) } +const DRAFT_WARNING: &str = "warning: --draft is not supported by this forge; opening a normal PR"; + fn is_remote_line(line: &str) -> bool { remote_content(line).is_some() } @@ -395,6 +402,18 @@ fn is_pr_hint_opener(line: &str) -> bool { mod tests { use super::*; + #[test] + fn draft_flag_parses() { + #[derive(clap::Parser)] + struct Cli { + #[command(flatten)] + args: Args, + } + use clap::Parser; + let cli = Cli::parse_from(["x", "--title", "t", "--head", "h", "--draft"]); + assert!(cli.args.draft); + } + #[test] fn agit_parse_pr_url_picks_pulls_link() { let stderr = "\ diff --git a/hive-forge/src/verbs/subscription.rs b/hive-forge/src/verbs/subscription.rs index 621a4c48..4e7b1129 100644 --- a/hive-forge/src/verbs/subscription.rs +++ b/hive-forge/src/verbs/subscription.rs @@ -3,7 +3,7 @@ //! repo the user watches (`--list`, for auditing the notification //! firehose). -use anyhow::Result; +use anyhow::{Result, bail}; use clap::Args as ClapArgs; use serde_json::json; @@ -21,7 +21,8 @@ pub struct Args { /// Subscribe (watch the repo). #[arg(long, group = "action")] watch: bool, - /// Mute (mark ignored). + /// Not supported: the forge API has no ignore endpoint. Errors; use + /// `--unwatch` to stop notifications. #[arg(long, group = "action")] ignore: bool, /// Unsubscribe (clear watch + ignore). @@ -32,14 +33,26 @@ pub struct Args { list: bool, } +/// `--ignore` has no Forgejo endpoint; fail rather than subscribe. +fn reject_ignore(args: &Args) -> Result<()> { + if args.ignore { + bail!( + "--ignore is not supported: the forge API has no ignore endpoint; \ + use `--unwatch` to stop notifications" + ); + } + Ok(()) +} + /// Run the `subscription` verb: `--list` all watched repos, set the /// current repo's watch (`--watch` / `--ignore` / `--unwatch`), or get /// its current state. /// /// # Errors -/// Propagates transport / non-2xx errors from the Forgejo client calls -/// and from `print_json`. +/// Fails on `--ignore` (unsupported). Propagates transport / non-2xx +/// errors from the Forgejo client calls and from `print_json`. pub fn run(client: &Client, args: Args) -> Result<()> { + reject_ignore(&args)?; if args.list { // List every repo the authed user watches, so an agent can audit // its notification firehose and decide what to `--unwatch`. Not @@ -72,12 +85,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> { println!("unsubscribed"); return Ok(()); } - if args.watch || args.ignore { - // Forgejo's PUT subscription endpoint takes no body — it always - // subscribes (watch). The old raw client sent a - // `{subscribed, ignored}` body which the server ignored, so - // `--ignore` has always behaved like `--watch` server-side; the - // typed call just makes that explicit. + if args.watch { + // Forgejo's PUT subscription endpoint takes no body: it always + // subscribes (watch). let resp = client .api() .user_current_put_subscription(owner, name) @@ -102,3 +112,28 @@ pub fn run(client: &Client, args: Args) -> Result<()> { Err(e) => Err(e.into()), } } + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser)] + struct Cli { + #[command(flatten)] + args: Args, + } + + #[test] + fn ignore_is_rejected_pointing_at_unwatch() { + let cli = Cli::parse_from(["x", "--ignore"]); + let err = reject_ignore(&cli.args).unwrap_err().to_string(); + assert!(err.contains("--unwatch"), "{err}"); + } + + #[test] + fn watch_is_not_rejected() { + let cli = Cli::parse_from(["x", "--watch"]); + assert!(reject_ignore(&cli.args).is_ok()); + } +}