#![allow(dead_code)]
use super::{types::*, GitHubClient};
use anyhow::Result;
use chrono::{Duration, Utc};
use futures::future::join_all;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tracing::{error, info, warn};
const MAX_CONCURRENT_SCANS: usize = 5;
pub struct GitHubScanner {
client: Arc<GitHubClient>,
semaphore: Arc<Semaphore>,
}
impl GitHubScanner {
pub fn new(client: GitHubClient) -> Self {
Self {
client: Arc::new(client),
semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS)),
}
}
pub async fn scan_all_repos(&self) -> Result<Vec<RepositoryScan>> {
info!("Starting full GitHub repository scan");
let repos = self.client.get_all_repos().await?;
info!("Found {} repositories to scan", repos.len());
let mut handles = Vec::new();
for repo in repos {
let client = self.client.clone();
let permit = self.semaphore.clone();
let handle = tokio::spawn(async move {
let _permit = permit.acquire().await?;
Self::scan_single_repo(&client, repo).await
});
handles.push(handle);
}
let results = join_all(handles).await;
let mut scans = Vec::new();
for result in results {
match result {
Ok(Ok(scan)) => scans.push(scan),
Ok(Err(e)) => warn!("Failed to scan repository: {}", e),
Err(e) => error!("Task panicked: {}", e),
}
}
info!("Completed scanning {} repositories", scans.len());
Ok(scans)
}
async fn scan_single_repo(client: &GitHubClient, repo: Repository) -> Result<RepositoryScan> {
info!("Scanning repository: {}/{}", repo.owner, repo.name);
let owner = repo.owner.clone();
let repo_name = repo.name.clone();
let branches = match client.get_branches(&owner, &repo_name).await {
Ok(branches) => branches,
Err(e) => {
warn!("Failed to get branches for {}: {}", repo_name, e);
Vec::new()
}
};
let since = Utc::now() - Duration::days(30);
let mut all_commits = Vec::new();
for branch in &branches {
if let Ok(commits) = client.get_recent_commits(&owner, &repo_name, &branch.name, Some(since)).await {
all_commits.extend(commits);
}
}
let language_stats = match client.get_language_stats(&owner, &repo_name).await {
Ok(stats) => stats,
Err(e) => {
warn!("Failed to get language stats for {}: {}", repo_name, e);
LanguageStats {
languages: Default::default(),
total_bytes: 0,
}
}
};
let pull_requests = match client.get_recent_prs(&owner, &repo_name, "all").await {
Ok(prs) => prs.into_iter()
.filter(|pr| pr.updated_at > since)
.collect(),
Err(e) => {
warn!("Failed to get PRs for {}: {}", repo_name, e);
Vec::new()
}
};
Ok(RepositoryScan {
repository: repo,
branches,
recent_commits: all_commits,
language_stats,
pull_requests,
scan_timestamp: Utc::now(),
})
}
pub async fn generate_summary(&self, scans: &[RepositoryScan]) -> GitHubSummary {
let mut summary = GitHubSummary::default();
summary.total_repos = scans.len();
summary.total_branches = scans.iter().map(|s| s.branches.len()).sum();
summary.total_commits_30d = scans.iter().map(|s| s.recent_commits.len()).sum();
summary.total_prs_open = scans.iter()
.map(|s| s.pull_requests.iter().filter(|pr| pr.state == "open").count())
.sum();
summary.total_prs_merged_30d = scans.iter()
.map(|s| s.pull_requests.iter()
.filter(|pr| pr.state == "closed" && pr.merged_at.is_some())
.count())
.sum();
let mut language_bytes: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
for scan in scans {
for (lang, (bytes, _)) in &scan.language_stats.languages {
*language_bytes.entry(lang.clone()).or_insert(0) += bytes;
}
}
let mut languages: Vec<_> = language_bytes.into_iter().collect();
languages.sort_by(|a, b| b.1.cmp(&a.1));
summary.top_languages = languages.into_iter().take(10).collect();
let mut repo_activity: Vec<_> = scans.iter()
.map(|s| (s.repository.full_name.clone(), s.recent_commits.len()))
.collect();
repo_activity.sort_by(|a, b| b.1.cmp(&a.1));
summary.most_active_repos = repo_activity.into_iter()
.take(5)
.map(|(name, _)| name)
.collect();
for scan in scans {
for commit in scan.recent_commits.iter().take(5) {
if let Some(date) = commit.committer_date {
summary.recent_activity.push(ActivityEvent {
event_type: "commit".to_string(),
repo: scan.repository.full_name.clone(),
description: format!("{}: {}",
commit.author_name.as_deref().unwrap_or("Unknown"),
commit.message.lines().next().unwrap_or("").chars().take(50).collect::<String>()),
timestamp: date,
url: None,
});
}
}
for pr in scan.pull_requests.iter().take(3) {
summary.recent_activity.push(ActivityEvent {
event_type: format!("pr_{}", pr.state),
repo: scan.repository.full_name.clone(),
description: format!("#{}: {}", pr.number, pr.title),
timestamp: pr.updated_at,
url: None,
});
}
}
summary.recent_activity.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
summary.recent_activity.truncate(20);
summary
}
pub async fn get_attention_needed(&self, scans: &[RepositoryScan]) -> Vec<String> {
let mut attention_items = Vec::new();
let now = Utc::now();
for scan in scans {
for branch in &scan.branches {
if let Some(last_commit) = branch.last_commit_date {
if now.signed_duration_since(last_commit).num_days() > 90
&& branch.name != scan.repository.default_branch {
attention_items.push(format!(
"{}: Branch '{}' is stale (last commit {} days ago)",
scan.repository.full_name,
branch.name,
now.signed_duration_since(last_commit).num_days()
));
}
}
}
for pr in &scan.pull_requests {
if pr.state == "open" {
let age = now.signed_duration_since(pr.created_at).num_days();
if age > 30 {
attention_items.push(format!(
"{}: PR #{} '{}' is open for {} days",
scan.repository.full_name,
pr.number,
pr.title,
age
));
}
}
}
}
attention_items
}
}