Skip to main content

rootle_github/client/
search.rs

1//! Repository, organization and code search.
2
3use super::{API, GitHubClient, urlencoding};
4use crate::types::{OrgRepoItem, SearchReposResponse, SearchUsersResponse};
5use rootle_provider::{ErrorKind, ProviderError, ProviderResult, SearchItem};
6
7impl GitHubClient {
8    /// Repo search + org search, merged: orgs first, then repos.
9    /// Returns provider-level items (the trait boundary type).
10    pub fn search(&self, query: &str) -> ProviderResult<Vec<SearchItem>> {
11        let q = urlencoding(query);
12        let mut out = Vec::new();
13
14        let orgs: SearchUsersResponse =
15            self.get(&format!("{API}/search/users?q={q}+type:org&per_page=5"))?;
16        out.extend(orgs.items.into_iter().map(|u| SearchItem::Org(u.login)));
17
18        let repos: SearchReposResponse =
19            self.get(&format!("{API}/search/repositories?q={q}&per_page=20"))?;
20        out.extend(
21            repos
22                .items
23                .into_iter()
24                .map(|r| SearchItem::Repo(r.full_name)),
25        );
26
27        Ok(out)
28    }
29
30    pub fn org_repos(&self, org: &str) -> ProviderResult<Vec<rootle_provider::RepoInfo>> {
31        let repos: Vec<OrgRepoItem> =
32            self.get(&format!("{API}/orgs/{org}/repos?per_page=100&sort=updated"))?;
33        Ok(repos
34            .into_iter()
35            .map(|r| rootle_provider::RepoInfo {
36                name: r.name,
37                description: r.description,
38                private: r.private,
39                archived: r.archived,
40                pushed_at: r.pushed_at,
41            })
42            .collect())
43    }
44
45    /// Code search (plans/0002 §4). Requires auth — anonymous clients
46    /// get a clear error. `q` is the full query string including
47    /// qualifiers (`repo:`, `org:`, `extension:`, `path:`); text-match
48    /// fragments are requested for previews.
49    pub fn search_code(&self, q: &str) -> ProviderResult<(Vec<crate::types::CodeItem>, bool)> {
50        let (items, total) = self.search_code_page(q, 1)?;
51        // GitHub caps code search at 1000 results — that is the
52        // provider's own truncation signal (plans/0008 §4).
53        Ok((items, total > 1000))
54    }
55
56    /// One page of code search (`per_page=100`, 1-indexed pages).
57    /// Returns (items, total_count) — total drives `truncated` and the
58    /// progressive page loop (v1.3, plans/0011).
59    pub fn search_code_page(
60        &self,
61        q: &str,
62        page: u32,
63    ) -> ProviderResult<(Vec<crate::types::CodeItem>, u64)> {
64        if self.is_anonymous() {
65            return Err(ProviderError::new(
66                ErrorKind::Auth,
67                "code search needs a token — set ROOTLE_TOKEN or log in with `gh`",
68            ));
69        }
70        let url = format!(
71            "{API}/search/code?q={}&per_page=100&page={page}",
72            urlencoding(q)
73        );
74        let resp: crate::types::SearchCodeResponse =
75            self.get_accept(&url, "application/vnd.github.text-match+json")?;
76        Ok((resp.items, resp.total_count))
77    }
78}