organizational_intelligence_plugin/
github.rs1use anyhow::{anyhow, Result};
5use chrono::{DateTime, Utc};
6use octocrab::Octocrab;
7use serde::{Deserialize, Serialize};
8use tracing::{debug, info};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RepoInfo {
13 pub name: String,
14 pub full_name: String,
15 pub description: Option<String>,
16 pub language: Option<String>,
17 pub stars: u32,
18 pub default_branch: String,
19 pub updated_at: DateTime<Utc>,
20}
21
22pub struct GitHubMiner {
25 client: Octocrab,
26}
27
28impl GitHubMiner {
29 pub fn new(token: Option<String>) -> Self {
45 let client = if let Some(token) = token {
46 debug!("Initializing GitHub client with authentication");
47 Octocrab::builder()
48 .personal_token(token)
49 .build()
50 .expect("Failed to build Octocrab client")
51 } else {
52 debug!("Initializing GitHub client without authentication");
53 Octocrab::builder()
54 .build()
55 .expect("Failed to build Octocrab client")
56 };
57
58 Self { client }
59 }
60
61 pub async fn fetch_organization_repos(&self, org_name: &str) -> Result<Vec<RepoInfo>> {
84 if org_name.trim().is_empty() {
86 return Err(anyhow!("Organization name cannot be empty"));
87 }
88
89 info!("Fetching repositories for organization: {}", org_name);
90
91 let repos = self
93 .client
94 .orgs(org_name)
95 .list_repos()
96 .send()
97 .await
98 .map_err(|e| anyhow!("Failed to fetch repositories for {}: {}", org_name, e))?;
99
100 debug!("Found {} repositories for {}", repos.items.len(), org_name);
101
102 let repo_infos: Vec<RepoInfo> = repos
104 .items
105 .into_iter()
106 .map(|repo| RepoInfo {
107 name: repo.name,
108 full_name: repo.full_name.unwrap_or_default(),
109 description: repo.description,
110 language: repo.language.and_then(|v| v.as_str().map(String::from)),
111 stars: repo.stargazers_count.unwrap_or(0),
112 default_branch: repo.default_branch.unwrap_or_else(|| "main".to_string()),
113 updated_at: repo.updated_at.unwrap_or_else(Utc::now),
114 })
115 .collect();
116
117 info!(
118 "Successfully fetched {} repositories for {}",
119 repo_infos.len(),
120 org_name
121 );
122
123 Ok(repo_infos)
124 }
125
126 pub fn filter_by_date(repos: Vec<RepoInfo>, since: DateTime<Utc>) -> Vec<RepoInfo> {
135 repos
136 .into_iter()
137 .filter(|repo| repo.updated_at >= since)
138 .collect()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[tokio::test]
147 async fn test_github_miner_creation() {
148 let _miner = GitHubMiner::new(None);
150 let _miner_with_token = GitHubMiner::new(Some("test_token".to_string()));
151 }
152
153 #[tokio::test]
154 async fn test_empty_org_name_validation() {
155 let miner = GitHubMiner::new(None);
156 let result = miner.fetch_organization_repos("").await;
157
158 assert!(result.is_err());
159 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
160 }
161
162 #[tokio::test]
163 async fn test_whitespace_org_name_validation() {
164 let miner = GitHubMiner::new(None);
165 let result = miner.fetch_organization_repos(" ").await;
166
167 assert!(result.is_err());
168 }
169}