From fce9bb8c11251c361c0b306c4fc661832b219778 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 24 Jun 2026 23:19:13 +0200 Subject: [PATCH] feat(#1985): subscription --list to audit watched repos --- docs/tools/forge.md | 1 + hive-forge/src/main.rs | 2 +- hive-forge/src/verbs/subscription.rs | 37 ++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/tools/forge.md b/docs/tools/forge.md index f6aa3f31..cde6a0fc 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -81,6 +81,7 @@ hive-forge ci-log --run 51 # print a CI run's job step logs (r hive-forge ci-rerun --pr 42 # re-run CI without an empty commit (dispatches a fresh run; --run n / --branch name also work) hive-forge subscription --watch # subscribe to repo notifications hive-forge subscription --unwatch # unsubscribe +hive-forge subscription --list # list every repo you watch (audit the notification firehose) hive-forge -r internal/knowledge clone # clone with creds auto-injected hive-forge -r internal/knowledge pr-create --agit --topic foo --title "..." # open PR via AGit (no fork) ``` diff --git a/hive-forge/src/main.rs b/hive-forge/src/main.rs index a733be23..eb617eac 100644 --- a/hive-forge/src/main.rs +++ b/hive-forge/src/main.rs @@ -142,7 +142,7 @@ enum Verb { /// Print the unified diff for a PR. #[command(hide = true)] Diff(verbs::diff::Args), - /// Get or set this user's watch subscription on a repo. + /// Get/set this user's watch subscription on a repo, or --list all watched repos. Subscription(verbs::subscription::Args), /// List timeline events on an issue or PR (closes, label adds, /// assignments, commit refs, pushes, etc.) — the audit trail diff --git a/hive-forge/src/verbs/subscription.rs b/hive-forge/src/verbs/subscription.rs index 451275db..abc0467e 100644 --- a/hive-forge/src/verbs/subscription.rs +++ b/hive-forge/src/verbs/subscription.rs @@ -1,5 +1,7 @@ -//! `subscription [--watch|--ignore|--unwatch] [repo]` — get or set -//! the current user's watch subscription for a repo. +//! `subscription [--watch|--ignore|--unwatch|--list] [repo]` — get or +//! set the current user's watch subscription for a repo, or list every +//! repo the user watches (`--list`, for auditing the notification +//! firehose). use anyhow::Result; use clap::Args as ClapArgs; @@ -9,6 +11,12 @@ use crate::client::Client; use crate::verbs::print_json; #[derive(ClapArgs)] +#[allow( + clippy::struct_excessive_bools, + reason = "mutually-exclusive clap action flags (group = \"action\"); an \ + enum would drop the --watch/--ignore/--unwatch/--list flag \ + ergonomics agents already use" +)] pub struct Args { /// Subscribe (watch the repo). #[arg(long, group = "action")] @@ -19,9 +27,34 @@ pub struct Args { /// Unsubscribe (clear watch + ignore). #[arg(long, group = "action")] unwatch: bool, + /// List every repo the current user watches (ignores `[repo]`). + #[arg(long, group = "action")] + list: bool, } pub fn run(client: &Client, args: Args) -> Result<()> { + if args.list { + // List every repo the authed user watches, so an agent can audit + // its notification firehose and decide what to `--unwatch`. Not + // repo-scoped. Paginated, with a generous page cap so a + // misbehaving server can't spin us forever. + const MAX_PAGES: u32 = 50; + const PAGE: u32 = 50; + let mut watching: Vec = Vec::new(); + for page in 1..=MAX_PAGES { + let v = client.get_json(&format!("/user/subscriptions?page={page}&limit={PAGE}"))?; + let arr = v.as_array().cloned().unwrap_or_default(); + let n = arr.len(); + watching.extend( + arr.iter() + .filter_map(|r| r["full_name"].as_str().map(str::to_owned)), + ); + if n < PAGE as usize { + break; + } + } + return print_json(&json!({ "watching": watching })); + } let repo = client.repo(); if args.unwatch { client.delete(&format!("/repos/{repo}/subscription"), None)?;