feat(hive-forge): repo-search verb — keyword/topic search across forge repos

This commit is contained in:
atlas 2026-07-10 15:14:48 +02:00 committed by mara
commit 327a3bc3bc
3 changed files with 91 additions and 0 deletions

View file

@ -113,6 +113,10 @@ enum Verb {
/// the valid names + descriptions for triage / labelling. `--json`
/// emits the full label objects (id, name, color, description).
RepoLabels(verbs::repo_labels::Args),
/// Search for repositories on the forge instance by keyword, topic, or
/// description. Not repo-scoped — queries the instance-wide explore
/// endpoint. `--json` emits the full repository objects.
RepoSearch(verbs::repo_search::Args),
/// Triage lint queries (unassigned / no-reviewer / stale-branches / assignments).
Lint(verbs::lint::Args),
/// List issues / PRs with filters (`--kind`, `--state`, `--assignee`,
@ -198,6 +202,7 @@ fn main() -> Result<()> {
Verb::RepoCreate(a) => verbs::repo_create::run(&client, a),
Verb::RepoAddCollaborator(a) => verbs::repo_add_collaborator::run(&client, a),
Verb::RepoLabels(a) => verbs::repo_labels::run(&client, a),
Verb::RepoSearch(a) => verbs::repo_search::run(&client, a),
Verb::Lint(a) => verbs::lint::run(&client, a),
Verb::List(a) => verbs::list::run(&client, a),
Verb::Milestone(a) => verbs::milestone::run(&client, a),

View file

@ -37,6 +37,7 @@ pub mod reopen;
pub mod repo_add_collaborator;
pub mod repo_create;
pub mod repo_labels;
pub mod repo_search;
pub mod subscription;
pub mod timeline;
pub mod tree_sha;

View file

@ -0,0 +1,85 @@
//! `repo-search [--query <kw>] [--topic] [--include-desc] [--limit N]` —
//! search for repositories on the forge instance.
//!
//! Wraps `GET /repos/search` (Forgejo explore endpoint). Useful for agents
//! that need to find a repo by keyword rather than knowing the exact
//! `owner/repo` slug upfront. Not repo-scoped — the `-r` flag is ignored.
//!
//! Human output: one `owner/repo description` line per hit. `--json`
//! emits the full `SearchResults.data` array as raw JSON.
use anyhow::Result;
use clap::Args as ClapArgs;
use forgejo_api::structs::RepoSearchQuery;
use crate::client::Client;
use crate::verbs::print_json;
#[derive(ClapArgs)]
pub struct Args {
/// Keyword to search for (matches repo name by default; combine with
/// `--include-desc` to also match description text).
#[arg(short = 'q', long)]
query: Option<String>,
/// Restrict matches to repositories that have the keyword as a **topic**
/// tag rather than in the name.
#[arg(long)]
topic: bool,
/// Extend the keyword search to repository descriptions (in addition to
/// names, or topics when `--topic` is set).
#[arg(long = "include-desc")]
include_desc: bool,
/// Maximum number of results to return (default: 30).
#[arg(long, default_value_t = 30, value_parser = clap::value_parser!(u64).range(1..))]
limit: u64,
}
/// # Errors
///
/// Propagates transport errors from the Forgejo search call or JSON
/// serialisation errors when emitting `--json` output.
pub fn run(client: &Client, args: Args) -> Result<()> {
let limit = u32::try_from(args.limit.min(u32::MAX as u64)).unwrap_or(u32::MAX);
let results = client
.api()
.repo_search(RepoSearchQuery {
q: args.query,
topic: args.topic.then_some(true),
include_desc: args.include_desc.then_some(true),
// All other filters left at defaults — callers who need them
// can drop to `--json` and post-filter.
uid: None,
priority_owner_id: None,
team_id: None,
starred_by: None,
private: None,
is_private: None,
template: None,
archived: None,
mode: None,
exclusive: None,
sort: None,
order: None,
})
.page_size(limit)
.send()?;
let repos = results.data.unwrap_or_default();
if client.json_mode() {
return print_json(&serde_json::to_value(&repos)?);
}
if repos.is_empty() {
eprintln!("(no results)");
return Ok(());
}
for repo in &repos {
let full_name = repo.full_name.as_deref().unwrap_or("?/?");
let desc = repo.description.as_deref().unwrap_or("");
if desc.is_empty() {
println!("{full_name}");
} else {
println!("{full_name} {desc}");
}
}
Ok(())
}