mod freshness;
mod registry;
mod types;
mod version;
use log::warn;
use octocrab::models::repos::dependabot::State;
use reqwest::Client;
use serde_json::Value as JsonValue;
use std::path::Path;
use std::time::Duration;
use toml::Value as TomlValue;
use crate::github::search_repositories::config::SearchConfig;
use crate::github::search_repositories::types::DependencyMetrics;
use freshness::calculate_dependency_freshness;
use registry::{check_cargo_outdated, check_npm_outdated, check_pypi_outdated};
pub(crate) async fn collect_dependency_metrics(
repo_path: &Path,
config: &SearchConfig,
octocrab: &octocrab::Octocrab,
owner: &str,
repo: &str,
) -> Option<DependencyMetrics> {
let mut direct_dependencies = 0u32;
let mut dev_dependencies = 0u32;
let mut package_managers = Vec::new();
let mut lock_files_present = false;
let mut cargo_deps: Option<toml::Table> = None;
let mut npm_deps: Option<serde_json::Map<String, JsonValue>> = None;
let mut python_reqs: Vec<String> = Vec::new();
if let Ok(content) = std::fs::read_to_string(repo_path.join("Cargo.toml")) {
package_managers.push("Cargo".to_string());
if let Ok(toml_value) = content.parse::<TomlValue>() {
if let Some(deps) = toml_value.get("dependencies").and_then(|v| v.as_table()) {
direct_dependencies += deps.len() as u32;
cargo_deps = Some(deps.clone());
}
if let Some(dev_deps) = toml_value
.get("dev-dependencies")
.and_then(|v| v.as_table())
{
dev_dependencies += dev_deps.len() as u32;
}
}
if repo_path.join("Cargo.lock").exists() {
lock_files_present = true;
}
}
if let Ok(content) = std::fs::read_to_string(repo_path.join("package.json")) {
package_managers.push("npm".to_string());
if let Ok(json_value) = serde_json::from_str::<JsonValue>(&content) {
if let Some(deps) = json_value.get("dependencies").and_then(|v| v.as_object()) {
direct_dependencies += deps.len() as u32;
npm_deps = Some(deps.clone());
}
if let Some(dev_deps) = json_value
.get("devDependencies")
.and_then(|v| v.as_object())
{
dev_dependencies += dev_deps.len() as u32;
}
}
if repo_path.join("package-lock.json").exists() || repo_path.join("yarn.lock").exists() {
lock_files_present = true;
}
}
if repo_path.join("requirements.txt").exists() {
package_managers.push("pip".to_string());
if let Ok(content) = std::fs::read_to_string(repo_path.join("requirements.txt")) {
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
direct_dependencies += 1;
python_reqs.push(trimmed.to_string());
}
}
}
}
if repo_path.join("Pipfile").exists() || repo_path.join("poetry.lock").exists() {
lock_files_present = true;
}
if repo_path.join("go.mod").exists() {
package_managers.push("Go modules".to_string());
if let Ok(content) = std::fs::read_to_string(repo_path.join("go.mod")) {
let mut in_require_block = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("require (") || trimmed == "require (" {
in_require_block = true;
continue;
}
if in_require_block && trimmed.starts_with(')') {
in_require_block = false;
continue;
}
if in_require_block && !trimmed.is_empty() && !trimmed.starts_with("//") {
direct_dependencies += 1;
}
if !in_require_block && trimmed.starts_with("require ") && !trimmed.contains('(') {
direct_dependencies += 1;
}
}
}
if repo_path.join("go.sum").exists() {
lock_files_present = true;
}
}
let total_dependencies = direct_dependencies + dev_dependencies;
let dependency_freshness_score = calculate_dependency_freshness(repo_path, &package_managers);
let vulnerable_dependencies = match octocrab
.repos(owner, repo)
.dependabot()
.per_page(config.api_page_size)
.get_alerts()
.await
{
Ok(page) => page
.items
.iter()
.filter(|alert| matches!(alert.state, State::Open))
.count() as u32,
Err(e) => {
warn!("Failed to fetch Dependabot alerts for {owner}/{repo}: {e} - defaulting to 0");
0
}
};
let outdated_dependencies = if total_dependencies > 50 {
0 } else {
let client = match Client::builder().timeout(Duration::from_secs(5)).build() {
Ok(c) => c,
Err(_) => {
return Some(DependencyMetrics {
total_dependencies,
direct_dependencies,
dev_dependencies,
outdated_dependencies: 0,
vulnerable_dependencies,
dependency_freshness_score,
package_managers,
lock_files_present,
});
}
};
let check_future = async {
let (cargo_count, npm_count, pypi_count) = tokio::join!(
async {
if let Some(ref deps) = cargo_deps {
check_cargo_outdated(deps, &client).await
} else {
0u32
}
},
async {
if let Some(ref deps) = npm_deps {
check_npm_outdated(deps, &client).await
} else {
0u32
}
},
async {
if python_reqs.is_empty() {
0u32
} else {
check_pypi_outdated(&python_reqs, &client).await
}
}
);
cargo_count + npm_count + pypi_count
};
if let Ok(count) = tokio::time::timeout(Duration::from_secs(10), check_future).await {
count
} else {
warn!("Timeout checking outdated dependencies for {owner}/{repo}");
0
}
};
Some(DependencyMetrics {
total_dependencies,
direct_dependencies,
dev_dependencies,
outdated_dependencies,
vulnerable_dependencies,
dependency_freshness_score,
package_managers,
lock_files_present,
})
}