//! `assign [--remove]` — add or remove a user from an //! issue/PR's assignee list. Forgejo has no dedicated POST endpoint — //! we read the current list, mutate, and PATCH the issue back (Forgejo //! has no such endpoint; matches the bash helper's logic). use anyhow::Result; use clap::Args as ClapArgs; use forgejo_api::structs::EditIssueOption; use serde_json::json; use crate::client::{Client, index}; use crate::verbs::print_json; #[derive(ClapArgs)] pub struct Args { /// Issue or PR number. pub(crate) number: u64, /// User login to assign (or unassign with `--remove`). user: String, /// Remove the user instead of adding. #[arg(long)] remove: bool, } pub fn run(client: &Client, args: Args) -> Result<()> { let (owner, name) = client.owner_repo()?; let idx = index(args.number)?; let current = client.api().issue_get_issue(owner, name, idx).send()?; let mut assignees: Vec = current .assignees .unwrap_or_default() .into_iter() .filter_map(|u| u.login) .collect(); if args.remove { assignees.retain(|u| u != &args.user); } else if !assignees.contains(&args.user) { assignees.push(args.user.clone()); } let payload = EditIssueOption { assignee: None, assignees: Some(assignees), body: None, due_date: None, milestone: None, r#ref: None, state: None, title: None, unset_due_date: None, updated_at: None, }; let resp = client .api() .issue_edit_issue(owner, name, idx, payload) .send()?; let logins: Vec<&str> = resp .assignees .as_deref() .unwrap_or_default() .iter() .filter_map(|u| u.login.as_deref()) .collect(); // Verify the mutation actually landed. Forgejo silently drops an assignee // change the caller isn't permitted to make (e.g. unassigning another // user) and echoes back the *unchanged* list with a 200 — so trusting the // success status reports a no-op as success. Diff the returned list // against the intent and fail loudly instead. let still_present = logins.iter().any(|&u| u == args.user); if args.remove && still_present { anyhow::bail!( "assignee '{}' is still assigned to #{} after remove — the forge \ rejected the change (likely insufficient permission to unassign \ this user). current assignees: [{}]", args.user, args.number, logins.join(", ") ); } if !args.remove && !still_present { anyhow::bail!( "assignee '{}' is not assigned to #{} after add — the forge rejected \ the change (the user may not exist or lack access to this repo). \ current assignees: [{}]", args.user, args.number, logins.join(", ") ); } print_json(&json!({ "number": resp.number, "assignees": logins, })) }