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 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 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 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 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(_) => {} Err(e) => {
103 tracing::warn!("Failed to fetch explicit repo {repo_name}: {e}");
104 }
105 }
106 }
107
108 Ok(matched)
109 }
110
111 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}