diff --git a/docs/tools/forge.md b/docs/tools/forge.md index e6fcdd0c..e9228a0a 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -21,7 +21,7 @@ under `issue` and `pr` parent commands — `hive-forge pr close 42`, refuses an issue number, which the old generic `close` couldn't). Run `hive-forge pr --help` / `hive-forge issue --help` for the full subcommand list (show/create/edit/status/merge/reviews/commits/diff/view/comment/ -comments/close/reopen/labels/assign/dependency/reaction/timeline as applicable). +comments/close/reopen/labels/assign/dependency/timeline as applicable). The flat forms below (`close 42`, `pr-create …`, `pr-status …`, …) still work as **hidden back-compat aliases** during the transition and are dropped from @@ -53,11 +53,6 @@ hive-forge issue dependency 42 # list #42's dependencies (issues hive-forge issue dependency 42 add 40 41 # #42 is now blocked by #40 and #41 hive-forge issue dependency 42 remove 40 # drop the #40 dependency link hive-forge pr dependency 55 add 42 # same verb, PR-scoped (Forgejo shares the issue/PR index) -hive-forge issue reaction 42 # list #42's reactions -hive-forge issue reaction 42 add heart # react to #42 with :heart: -hive-forge issue reaction 42 --comment 18042 add +1 # react to a specific comment instead -hive-forge issue reaction 42 remove heart # remove your own :heart: reaction -hive-forge issue reaction 1 --list-allowed # this instance's actual configured shortcodes 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" @@ -292,18 +287,6 @@ to discover valid label names before triaging or to audit the label set. forge UI's dependency panel). Same-repo only, matching the web UI (no cross-repo dependency support). `list` (the default action) and `add`/ `remove` all print the resulting dependency list as JSON. -- `issue reaction add ` / `pr reaction add ` add - an emoji reaction (Forgejo's shortcode vocabulary — `+1`, `heart`, - `rocket`, …, same set the web UI's picker offers, not a raw emoji - character); `--comment ` targets a specific comment's reactions - instead of the issue/PR itself. `list`/`add`/`remove` all print the - resulting reaction list as JSON, same shape as `dependency`. `view`, - `issue`, `pr`, and `comment-show` always show a reaction summary; - `comments --show-reactions` adds one per comment shown (opt-in — one - extra request per comment, no server-side inline count). `reaction - --list-allowed` prints the instance's actual configured shortcode - list (`GET /settings/ui`'s `allowed_reactions`) instead of guessing — - instance-global, ``/`--comment` are ignored on this path. - Do NOT use raw `curl` for forge access -- the CLI handles auth, error checking, and output formatting. - `issue-create --label ` / `pr-create --label ` are diff --git a/hive-forge/src/main.rs b/hive-forge/src/main.rs index 736d9942..b5a44b27 100644 --- a/hive-forge/src/main.rs +++ b/hive-forge/src/main.rs @@ -67,7 +67,7 @@ enum Verb { /// Dump title + body + all comments for an issue or PR. #[command(hide = true)] View(verbs::view::Args), - /// Issue-scoped commands: `issue …`. + /// Issue-scoped commands: `issue …`. Issue(verbs::issue_cmd::Args), /// Create an issue. Prints the issue URL on success. #[command(hide = true)] @@ -75,7 +75,7 @@ enum Verb { /// Edit an issue's title, body, state, or milestone. #[command(hide = true)] IssueEdit(verbs::issue_edit::Args), - /// PR-scoped commands: `pr …`. + /// PR-scoped commands: `pr …`. Pr(verbs::pr_cmd::Args), /// List a PR's commits as JSON (sha, message, author date, author). #[command(hide = true)] diff --git a/hive-forge/src/verbs/comment_show.rs b/hive-forge/src/verbs/comment_show.rs index 4f45facc..e3e61e1e 100644 --- a/hive-forge/src/verbs/comment_show.rs +++ b/hive-forge/src/verbs/comment_show.rs @@ -6,9 +6,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{ - attachment_json, attachment_line, comment_reactions, print_json, reaction_summary, rfc3339, -}; +use crate::verbs::{attachment_json, attachment_line, print_json, rfc3339}; #[derive(ClapArgs)] pub struct Args { @@ -25,7 +23,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { else { bail!("hive-forge comment-show: comment {} not found", args.id); }; - let reactions = comment_reactions(client, owner, name, args.id)?; if client.json_mode() { let trimmed = json!({ "id": c.id, @@ -35,7 +32,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "body": c.body, "url": c.html_url, "attachments": attachment_json(c.assets.as_deref()), - "reactions": reactions, }); print_json(&trimmed) } else { @@ -46,9 +42,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { println!("{line}"); } } - if let Some(summary) = reaction_summary(&reactions) { - println!("[reactions: {summary}]"); - } Ok(()) } } diff --git a/hive-forge/src/verbs/comments.rs b/hive-forge/src/verbs/comments.rs index 1d7a3190..8066e2d0 100644 --- a/hive-forge/src/verbs/comments.rs +++ b/hive-forge/src/verbs/comments.rs @@ -14,9 +14,11 @@ //! always reported** — never a silent truncation. //! - `--since ` filters to comments at or after that //! timestamp instead of a head/tail window — a cursor: feed the -//! last-seen row's own `created_at` back in next time. Mutually -//! exclusive with `--tail`; `--limit` clamps like `timeline`'s own -//! does (see `crate::verbs::MAX_LIMIT`). +//! last-seen row's own `created_at` back in next time to fetch only +//! what's new. Mutually exclusive with `--tail`. No total exists for +//! a since-filtered query, so this uses the same over-fetch-by-one +//! trick `timeline`'s `--limit` does, and `--limit` is clamped the +//! same way (see `crate::verbs::MAX_LIMIT`). //! //! Review *bodies* on PRs are always merged in too (`pulls//reviews` //! isn't the issues/comments thread), tagged `[review: STATE]`, with a @@ -25,8 +27,7 @@ //! //! `--json` output is an object (`{"comments": [...], "more_before": N, //! "more_after": N, "since_more": bool}`), not a bare array, so a -//! script can read the truncation info too. `--show-reactions` adds a -//! per-comment reaction summary — see that flag's own doc for the cost. +//! script can read the truncation info too. use anyhow::Result; use clap::Args as ClapArgs; @@ -36,10 +37,7 @@ use time::OffsetDateTime; use crate::client::{Client, index}; use crate::notify; -use crate::verbs::{ - MAX_LIMIT, PAGE_SIZE, clamp_limit, comment_reactions, parse_rfc3339, print_json, - reaction_summary, rfc3339, -}; +use crate::verbs::{MAX_LIMIT, PAGE_SIZE, clamp_limit, parse_rfc3339, print_json, rfc3339}; #[derive(ClapArgs)] pub struct Args { @@ -60,10 +58,6 @@ pub struct Args { /// `--tail`. #[arg(long)] since: Option, - /// Fetch + display each shown comment's reaction summary. Costs one - /// extra request per comment shown — opt-in, not the default. - #[arg(long)] - show_reactions: bool, } pub fn run(client: &Client, args: Args) -> Result<()> { @@ -96,12 +90,10 @@ pub fn run(client: &Client, args: Args) -> Result<()> { }; // Merge in PR review bodies (empty for issues — degrades to a // no-op) so review feedback isn't silently dropped. - let mut comments = merge_chronological(thread, fetch_review_bodies(client, args.number)); + let comments = merge_chronological(thread, fetch_review_bodies(client, args.number)); // Reading the thread clears its unread notification so the // read-before-comment guard (in `comment`) lets a reply through. notify::mark_read_best_effort(client, repo, args.number); - let (owner, name) = client.owner_repo()?; - attach_reactions(client, owner, name, &mut comments, args.show_reactions); if client.json_mode() { let trimmed: Vec = comments .iter() @@ -117,7 +109,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "state": c.get("state"), "comments_count": c.get("comments_count"), "attachments": attachment_json_from_value(c), - "reactions": c.get("reactions").cloned().unwrap_or_else(|| json!([])), }) }) .collect(); @@ -154,13 +145,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { for line in attachment_lines_from_value(c) { println!("{line}"); } - if let Some(summary) = c - .get("reactions") - .and_then(Value::as_array) - .and_then(|r| reaction_summary(r)) - { - println!("[reactions: {summary}]"); - } println!(); } if since_clamped { @@ -200,39 +184,6 @@ fn truncation_note(more_before: usize, more_after: usize) -> Option { } } -/// Fetch + attach each shown comment's reaction list under a -/// `"reactions"` key, mutating in place so both the `--json` and -/// pretty-print branches see the same fetch. No-op when `show_reactions` -/// is false (the default) — the whole point of gating this behind a -/// flag is that a plain `comments` call costs nothing extra. Review -/// entries are skipped (synthesized, never have real reactions); a -/// per-comment fetch failure is swallowed rather than failing the whole -/// listing over one comment. -fn attach_reactions( - client: &Client, - owner: &str, - name: &str, - comments: &mut [Value], - show_reactions: bool, -) { - if !show_reactions { - return; - } - for c in comments.iter_mut() { - if c.get("kind").and_then(Value::as_str) == Some("review") { - continue; - } - let Some(id) = c.get("id").and_then(Value::as_u64) else { - continue; - }; - if let Ok(reactions) = comment_reactions(client, owner, name, id) - && let Some(obj) = c.as_object_mut() - { - obj.insert("reactions".to_owned(), json!(reactions)); - } - } -} - /// Attachment display lines for a single comment's already-serialized /// `assets` field — the shape this module's merge/render pipeline works /// on (comments arrive as `Value`s, not the typed `Comment` struct, once diff --git a/hive-forge/src/verbs/issue.rs b/hive-forge/src/verbs/issue.rs index 50c8a692..2f31796c 100644 --- a/hive-forge/src/verbs/issue.rs +++ b/hive-forge/src/verbs/issue.rs @@ -5,7 +5,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{attachment_json, dependency_summaries, issue_reactions, print_json}; +use crate::verbs::{attachment_json, dependency_summaries, print_json}; #[derive(ClapArgs)] pub struct Args { @@ -34,7 +34,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .filter_map(|l| l.name.as_deref()) .collect(); let dependencies = dependency_summaries(client, owner, name, args.number)?; - let reactions = issue_reactions(client, owner, name, args.number)?; let trimmed = json!({ "number": issue.number, "title": issue.title, @@ -43,7 +42,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "assignees": assignees, "labels": labels, "dependencies": dependencies, - "reactions": reactions, "body": issue.body, "attachments": attachment_json(issue.assets.as_deref()), }); diff --git a/hive-forge/src/verbs/issue_cmd.rs b/hive-forge/src/verbs/issue_cmd.rs index 168ab2e3..ec9fd864 100644 --- a/hive-forge/src/verbs/issue_cmd.rs +++ b/hive-forge/src/verbs/issue_cmd.rs @@ -42,9 +42,6 @@ enum Cmd { Assign(verbs::assign::Args), /// List / add / remove dependencies (issues this one is blocked by). Dependency(verbs::dependency::Args), - /// List / add / remove emoji reactions on the issue, or on one of its - /// comments with `--comment `. - Reaction(verbs::reaction::Args), /// List timeline events. Timeline(verbs::timeline::Args), } @@ -88,15 +85,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { assert_kind(client, a.number, Kind::Issue)?; verbs::dependency::run(client, a) } - Cmd::Reaction(a) => { - // `--list-allowed` is instance-global, not issue-scoped — skip - // the kind-assert rather than force a real issue number on a - // call that never uses it. - if !a.list_allowed { - assert_kind(client, a.number, Kind::Issue)?; - } - verbs::reaction::run(client, a) - } Cmd::Timeline(a) => { assert_kind(client, a.number, Kind::Issue)?; verbs::timeline::run(client, a) diff --git a/hive-forge/src/verbs/mod.rs b/hive-forge/src/verbs/mod.rs index df0f3f6f..0a49f161 100644 --- a/hive-forge/src/verbs/mod.rs +++ b/hive-forge/src/verbs/mod.rs @@ -34,7 +34,6 @@ pub mod pr_create; pub mod pr_merge; pub mod pr_reviews; pub mod pr_status; -pub mod reaction; pub mod reopen; pub mod repo_add_collaborator; pub mod repo_create; @@ -48,7 +47,7 @@ pub mod view; use std::fmt::Write as _; use anyhow::Result; -use forgejo_api::structs::{Attachment, Reaction}; +use forgejo_api::structs::Attachment; use serde_json::{Value, json}; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; @@ -381,79 +380,6 @@ pub(crate) fn dependency_summaries( .collect()) } -/// The current reactions on an issue or PR itself (not a comment) — each -/// entry's `content` is Forgejo's shortcode (`"+1"`, `"heart"`, …, the -/// same vocabulary GitHub uses), `user` its login. Same shared-index -/// rationale as [`dependency_summaries`]: PRs are issues internally, so -/// `issue`/`pr`/`view` and the `reaction` verb's own listing all call -/// this instead of duplicating the fetch-and-shape step. -/// -/// # Errors -/// -/// Propagates the forge API errors from listing reactions. -pub(crate) fn issue_reactions( - client: &Client, - owner: &str, - name: &str, - number: u64, -) -> Result> { - let (_, reactions) = client - .api() - .issue_get_issue_reactions(owner, name, index(number)?) - .send()?; - Ok(reaction_values(reactions)) -} - -/// The current reactions on a single comment, by comment id (not the -/// parent issue/PR number — Forgejo's comment-reaction endpoints are -/// keyed on the comment alone, same as [`comment_show`]'s lookup). -/// -/// # Errors -/// -/// Propagates the forge API errors from listing reactions. -pub(crate) fn comment_reactions( - client: &Client, - owner: &str, - name: &str, - comment_id: u64, -) -> Result> { - let reactions = client - .api() - .issue_get_comment_reactions(owner, name, index(comment_id)?) - .send()?; - Ok(reaction_values(reactions)) -} - -fn reaction_values(reactions: Vec) -> Vec { - reactions - .into_iter() - .map(|r| json!({ "content": r.content, "user": r.user.and_then(|u| u.login) })) - .collect() -} - -/// A one-line `content×count` summary of a reaction list (e.g. `+1×2 -/// heart×1`), grouped and sorted by content name for a stable rendering. -/// `None` for an empty list, so a row/section with no reactions omits the -/// summary entirely rather than printing something empty. -pub(crate) fn reaction_summary(reactions: &[Value]) -> Option { - let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new(); - for r in reactions { - if let Some(content) = r.get("content").and_then(Value::as_str) { - *counts.entry(content).or_insert(0) += 1; - } - } - if counts.is_empty() { - return None; - } - Some( - counts - .into_iter() - .map(|(content, n)| format!("{content}×{n}")) - .collect::>() - .join(" "), - ) -} - /// A single attachment as one display line — `[file: ] `, /// mirroring the `[file: ...]` marker convention `read_room` already /// uses for matrix attachments. `None` when the attachment has no diff --git a/hive-forge/src/verbs/pr.rs b/hive-forge/src/verbs/pr.rs index 0302d218..0e3d7c4c 100644 --- a/hive-forge/src/verbs/pr.rs +++ b/hive-forge/src/verbs/pr.rs @@ -5,7 +5,7 @@ use clap::Args as ClapArgs; use serde_json::json; use crate::client::{Client, index}; -use crate::verbs::{dependency_summaries, issue_reactions, print_json}; +use crate::verbs::{dependency_summaries, print_json}; #[derive(ClapArgs)] pub struct Args { @@ -20,7 +20,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .repo_get_pull_request(owner, name, index(args.number)?) .send()?; let dependencies = dependency_summaries(client, owner, name, args.number)?; - let reactions = issue_reactions(client, owner, name, args.number)?; let trimmed = json!({ "number": pull.number, "title": pull.title, @@ -31,7 +30,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { "head_branch": pull.head.as_ref().and_then(|h| h.label.as_deref()), "base_branch": pull.base.as_ref().and_then(|b| b.label.as_deref()), "dependencies": dependencies, - "reactions": reactions, }); print_json(&trimmed) } diff --git a/hive-forge/src/verbs/pr_cmd.rs b/hive-forge/src/verbs/pr_cmd.rs index 19574b38..de94548c 100644 --- a/hive-forge/src/verbs/pr_cmd.rs +++ b/hive-forge/src/verbs/pr_cmd.rs @@ -57,9 +57,6 @@ enum Cmd { AssignCommitter(verbs::assign::Args), /// List / add / remove dependencies (issues/PRs this one is blocked by). Dependency(verbs::dependency::Args), - /// List / add / remove emoji reactions on the PR, or on one of its - /// comments with `--comment `. - Reaction(verbs::reaction::Args), /// List timeline events. Timeline(verbs::timeline::Args), } @@ -112,15 +109,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { assert_kind(client, a.number, Kind::Pr)?; verbs::dependency::run(client, a) } - Cmd::Reaction(a) => { - // `--list-allowed` is instance-global, not PR-scoped — skip the - // kind-assert rather than force a real PR number on a call that - // never uses it. - if !a.list_allowed { - assert_kind(client, a.number, Kind::Pr)?; - } - verbs::reaction::run(client, a) - } Cmd::Timeline(a) => { assert_kind(client, a.number, Kind::Pr)?; verbs::timeline::run(client, a) diff --git a/hive-forge/src/verbs/reaction.rs b/hive-forge/src/verbs/reaction.rs deleted file mode 100644 index bde19d12..00000000 --- a/hive-forge/src/verbs/reaction.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! `reaction [--comment ] [list|add|remove] [content]` — -//! list, add, or remove emoji reactions on an issue/PR or one of its -//! comments. -//! -//! Forgejo's reaction endpoints work on the shared issue/PR index (PRs -//! are issues internally, same as `dependency`) when no `--comment` is -//! given; `--comment ` redirects to the separate comment-reaction -//! endpoint pair, keyed on the comment's own id (not the parent -//! issue/PR number — `number` still has to be passed so this verb slots -//! into `issue reaction` / `pr reaction` like every other -//! generic verb, but it's otherwise unused on that path). -//! -//! `content` is Forgejo's reaction shortcode vocabulary (`+1`, `-1`, -//! `laugh`, `confused`, `heart`, `hooray`, `rocket`, `eyes`, …), not a -//! raw emoji character — same set the forge web UI's reaction picker -//! offers. `--list-allowed` prints the *actual* instance-configured -//! list (`GET /settings/ui`'s `allowed_reactions`) instead of leaving a -//! caller to guess it or learn it from a 403 — this is instance-global, -//! not issue/PR-scoped, so `number`/`--comment` are ignored when it's -//! set (the number is still required positionally, same as every other -//! generic verb, but unused on this path). - -use anyhow::Result; -use clap::{Args as ClapArgs, Subcommand}; -use forgejo_api::structs::EditReactionOption; - -use crate::client::{Client, index}; -use crate::verbs::{comment_reactions, issue_reactions, print_json}; - -#[derive(ClapArgs)] -pub struct Args { - /// Issue or PR number. Ignored (but still required) when - /// `--list-allowed` is set. - pub(crate) number: u64, - /// Target a specific comment's reactions instead of the issue/PR - /// itself — the comment's own id (from `comments`/`comment-show`), - /// not its position in the thread. - #[arg(long)] - comment: Option, - /// Print the instance's actual configured reaction shortcodes - /// (`GET /settings/ui`) instead of listing/adding/removing — - /// instance-global, ignores `number`/`--comment`. - #[arg(long)] - pub(crate) list_allowed: bool, - #[command(subcommand)] - action: Option, -} - -#[derive(Subcommand)] -enum Action { - /// List reactions (default when no action is given). - List, - /// Add a reaction — a Forgejo shortcode, e.g. `+1`, `heart`, `rocket`. - Add { content: String }, - /// Remove your own reaction with this content. - Remove { content: String }, -} - -pub fn run(client: &Client, args: Args) -> Result<()> { - if args.list_allowed { - let settings = client.api().get_general_ui_settings().send()?; - return print_json(&serde_json::json!( - settings.allowed_reactions.unwrap_or_default() - )); - } - let (owner, name) = client.owner_repo()?; - match args.action.unwrap_or(Action::List) { - Action::List => {} - Action::Add { content } => { - let body = EditReactionOption { - content: Some(content), - }; - match args.comment { - Some(id) => { - client - .api() - .issue_post_comment_reaction(owner, name, index(id)?, body) - .send()?; - } - None => { - client - .api() - .issue_post_issue_reaction(owner, name, index(args.number)?, body) - .send()?; - } - } - } - Action::Remove { content } => { - let body = EditReactionOption { - content: Some(content), - }; - match args.comment { - Some(id) => { - client - .api() - .issue_delete_comment_reaction(owner, name, index(id)?, body) - .send()?; - } - None => { - client - .api() - .issue_delete_issue_reaction(owner, name, index(args.number)?, body) - .send()?; - } - } - } - } - let reactions = match args.comment { - Some(id) => comment_reactions(client, owner, name, id)?, - None => issue_reactions(client, owner, name, args.number)?, - }; - print_json(&serde_json::json!(reactions)) -} diff --git a/hive-forge/src/verbs/view.rs b/hive-forge/src/verbs/view.rs index 63fc226c..5f46da1d 100644 --- a/hive-forge/src/verbs/view.rs +++ b/hive-forge/src/verbs/view.rs @@ -7,7 +7,7 @@ use forgejo_api::structs::{IssueGetCommentsQuery, StateType}; use crate::client::{Client, index}; use crate::notify; -use crate::verbs::{attachment_line, issue_reactions, reaction_summary}; +use crate::verbs::attachment_line; #[derive(ClapArgs)] pub struct Args { @@ -53,10 +53,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> { println!("{line}"); } } - let reactions = issue_reactions(client, owner, name, args.number)?; - if let Some(summary) = reaction_summary(&reactions) { - println!("[reactions: {summary}]"); - } let (_, comments) = client .api() .issue_get_comments(