Skip to main content

ward/github/
repos.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3use serde::Deserialize;
4
5use super::Client;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct Repository {
9    pub name: String,
10    pub full_name: String,
11    pub archived: bool,
12    pub default_branch: String,
13    #[serde(default)]
14    pub description: Option<String>,
15    pub visibility: String,
16    #[serde(default)]
17    pub language: Option<String>,
18}
19
20impl Client {
21    /// List all repositories for the configured org, handling pagination.
22    pub async fn list_repos(&self) -> Result<Vec<Repository>> {
23        let mut all_repos = Vec::new();
24        let mut page = 1u32;
25
26        loop {
27            let resp = self
28                .get(&format!(
29                    "/orgs/{}/repos?per_page=100&page={page}&type=all",
30                    self.org
31                ))
32                .await?;
33
34            let status = resp.status();
35            if !status.is_success() {
36                let body = resp.text().await.unwrap_or_default();
37                anyhow::bail!("Failed to list repos (HTTP {status}): {body}");
38            }
39
40            let repos: Vec<Repository> = resp
41                .json()
42                .await
43                .context("Failed to parse repo list response")?;
44
45            if repos.is_empty() {
46                break;
47            }
48
49            all_repos.extend(repos);
50            page += 1;
51        }
52
53        Ok(all_repos)
54    }
55
56    /// List repos filtered by system ID prefix and/or explicit repo names,
57    /// with exclude patterns applied to the combined result.
58    pub async fn list_repos_for_system(
59        &self,
60        system_id: &str,
61        exclude_patterns: &[String],
62        explicit_repos: &[String],
63    ) -> Result<Vec<Repository>> {
64        let all = self.list_repos().await?;
65
66        let exclude_regex = if exclude_patterns.is_empty() {
67            None
68        } else {
69            let pattern = exclude_patterns.join("|");
70            Some(Regex::new(&pattern).context("Invalid exclude pattern regex")?)
71        };
72
73        // Start with prefix-matched repos
74        let mut matched: Vec<Repository> = all
75            .into_iter()
76            .filter(|r| !r.archived)
77            .filter(|r| r.name.starts_with(system_id))
78            .filter(|r| {
79                if let Some(ref re) = exclude_regex {
80                    let suffix = r
81                        .name
82                        .strip_prefix(system_id)
83                        .and_then(|s| s.strip_prefix('-'))
84                        .unwrap_or(&r.name);
85                    !re.is_match(suffix)
86                } else {
87                    true
88                }
89            })
90            .collect();
91
92        // Add explicit repos (fetch individually, skip if already matched by prefix)
93        for repo_name in explicit_repos {
94            if matched.iter().any(|r| r.name == *repo_name) {
95                continue;
96            }
97            match self.get_repo(repo_name).await {
98                Ok(repo) if !repo.archived => {
99                    matched.push(repo);
100                }
101                Ok(_) => {} // archived, skip
102                Err(e) => {
103                    tracing::warn!("Failed to fetch explicit repo {repo_name}: {e}");
104                }
105            }
106        }
107
108        Ok(matched)
109    }
110
111    /// Get a single repository.
112    pub async fn get_repo(&self, repo_name: &str) -> Result<Repository> {
113        let resp = self
114            .get(&format!("/repos/{}/{repo_name}", self.org))
115            .await?;
116
117        let status = resp.status();
118        if !status.is_success() {
119            let body = resp.text().await.unwrap_or_default();
120            anyhow::bail!("Failed to get repo {repo_name} (HTTP {status}): {body}");
121        }
122
123        resp.json().await.context("Failed to parse repo response")
124    }
125}