Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9cc5635a | ||
|
|
96a27cef3c | ||
|
|
2c45a9960f |
11 changed files with 308 additions and 16 deletions
|
|
@ -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/timeline as applicable).
|
||||
comments/close/reopen/labels/assign/dependency/reaction/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,6 +53,11 @@ 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"
|
||||
|
|
@ -287,6 +292,18 @@ 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 <n> add <content>` / `pr reaction <n> add <content>` 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 <id>` 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
|
||||
<n> --list-allowed` prints the instance's actual configured shortcode
|
||||
list (`GET /settings/ui`'s `allowed_reactions`) instead of guessing —
|
||||
instance-global, `<n>`/`--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 <name>` / `pr-create --label <name>` are
|
||||
|
|
|
|||
|
|
@ -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 <show|create|edit|view|comment|comments|close|reopen|labels|assign|dependency|timeline> …`.
|
||||
/// Issue-scoped commands: `issue <show|create|edit|view|comment|comments|close|reopen|labels|assign|dependency|reaction|timeline> …`.
|
||||
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 <show|status|create|merge|reviews|assign-reviewer|commits|diff|view|edit|comment|comments|close|reopen|labels|assign-committer|dependency|timeline> …`.
|
||||
/// PR-scoped commands: `pr <show|status|create|merge|reviews|assign-reviewer|commits|diff|view|edit|comment|comments|close|reopen|labels|assign-committer|dependency|reaction|timeline> …`.
|
||||
Pr(verbs::pr_cmd::Args),
|
||||
/// List a PR's commits as JSON (sha, message, author date, author).
|
||||
#[command(hide = true)]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use clap::Args as ClapArgs;
|
|||
use serde_json::json;
|
||||
|
||||
use crate::client::{Client, index};
|
||||
use crate::verbs::{attachment_json, attachment_line, print_json, rfc3339};
|
||||
use crate::verbs::{
|
||||
attachment_json, attachment_line, comment_reactions, print_json, reaction_summary, rfc3339,
|
||||
};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -23,6 +25,7 @@ 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,
|
||||
|
|
@ -32,6 +35,7 @@ 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 {
|
||||
|
|
@ -42,6 +46,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
println!("{line}");
|
||||
}
|
||||
}
|
||||
if let Some(summary) = reaction_summary(&reactions) {
|
||||
println!("[reactions: {summary}]");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,9 @@
|
|||
//! always reported** — never a silent truncation.
|
||||
//! - `--since <RFC3339>` 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 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`).
|
||||
//! 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`).
|
||||
//!
|
||||
//! Review *bodies* on PRs are always merged in too (`pulls/<n>/reviews`
|
||||
//! isn't the issues/comments thread), tagged `[review: STATE]`, with a
|
||||
|
|
@ -27,7 +25,8 @@
|
|||
//!
|
||||
//! `--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.
|
||||
//! script can read the truncation info too. `--show-reactions` adds a
|
||||
//! per-comment reaction summary — see that flag's own doc for the cost.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Args as ClapArgs;
|
||||
|
|
@ -37,7 +36,10 @@ use time::OffsetDateTime;
|
|||
|
||||
use crate::client::{Client, index};
|
||||
use crate::notify;
|
||||
use crate::verbs::{MAX_LIMIT, PAGE_SIZE, clamp_limit, parse_rfc3339, print_json, rfc3339};
|
||||
use crate::verbs::{
|
||||
MAX_LIMIT, PAGE_SIZE, clamp_limit, comment_reactions, parse_rfc3339, print_json,
|
||||
reaction_summary, rfc3339,
|
||||
};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -58,6 +60,10 @@ pub struct Args {
|
|||
/// `--tail`.
|
||||
#[arg(long)]
|
||||
since: Option<String>,
|
||||
/// 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<()> {
|
||||
|
|
@ -90,10 +96,12 @@ 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 comments = merge_chronological(thread, fetch_review_bodies(client, args.number));
|
||||
let mut 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<Value> = comments
|
||||
.iter()
|
||||
|
|
@ -109,6 +117,7 @@ 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();
|
||||
|
|
@ -145,6 +154,13 @@ 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 {
|
||||
|
|
@ -184,6 +200,39 @@ fn truncation_note(more_before: usize, more_after: usize) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -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, print_json};
|
||||
use crate::verbs::{attachment_json, dependency_summaries, issue_reactions, print_json};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -34,6 +34,7 @@ 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,
|
||||
|
|
@ -42,6 +43,7 @@ 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()),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ 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 <id>`.
|
||||
Reaction(verbs::reaction::Args),
|
||||
/// List timeline events.
|
||||
Timeline(verbs::timeline::Args),
|
||||
}
|
||||
|
|
@ -85,6 +88,15 @@ 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)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ 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;
|
||||
|
|
@ -47,7 +48,7 @@ pub mod view;
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use anyhow::Result;
|
||||
use forgejo_api::structs::Attachment;
|
||||
use forgejo_api::structs::{Attachment, Reaction};
|
||||
use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
|
@ -380,6 +381,79 @@ 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<Vec<Value>> {
|
||||
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<Vec<Value>> {
|
||||
let reactions = client
|
||||
.api()
|
||||
.issue_get_comment_reactions(owner, name, index(comment_id)?)
|
||||
.send()?;
|
||||
Ok(reaction_values(reactions))
|
||||
}
|
||||
|
||||
fn reaction_values(reactions: Vec<Reaction>) -> Vec<Value> {
|
||||
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<String> {
|
||||
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::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
}
|
||||
|
||||
/// A single attachment as one display line — `[file: <name>] <url>`,
|
||||
/// mirroring the `[file: ...]` marker convention `read_room` already
|
||||
/// uses for matrix attachments. `None` when the attachment has no
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use clap::Args as ClapArgs;
|
|||
use serde_json::json;
|
||||
|
||||
use crate::client::{Client, index};
|
||||
use crate::verbs::{dependency_summaries, print_json};
|
||||
use crate::verbs::{dependency_summaries, issue_reactions, print_json};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -20,6 +20,7 @@ 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,
|
||||
|
|
@ -30,6 +31,7 @@ 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ 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 <id>`.
|
||||
Reaction(verbs::reaction::Args),
|
||||
/// List timeline events.
|
||||
Timeline(verbs::timeline::Args),
|
||||
}
|
||||
|
|
@ -109,6 +112,15 @@ 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)
|
||||
|
|
|
|||
113
hive-forge/src/verbs/reaction.rs
Normal file
113
hive-forge/src/verbs/reaction.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//! `reaction <number> [--comment <id>] [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 <id>` 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 <n> reaction` / `pr <n> 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<u64>,
|
||||
/// 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<Action>,
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ use forgejo_api::structs::{IssueGetCommentsQuery, StateType};
|
|||
|
||||
use crate::client::{Client, index};
|
||||
use crate::notify;
|
||||
use crate::verbs::attachment_line;
|
||||
use crate::verbs::{attachment_line, issue_reactions, reaction_summary};
|
||||
|
||||
#[derive(ClapArgs)]
|
||||
pub struct Args {
|
||||
|
|
@ -53,6 +53,10 @@ 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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue