use crate::error::{GvcError, Result};
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Repository {
pub name: String,
pub url: String,
pub group_filters: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GradleConfig {
pub repositories: Vec<Repository>,
}
pub struct GradleConfigParser {
project_path: PathBuf,
}
impl GradleConfigParser {
pub fn new<P: AsRef<Path>>(project_path: P) -> Self {
Self {
project_path: project_path.as_ref().to_path_buf(),
}
}
pub fn parse(&self) -> Result<GradleConfig> {
let mut repositories = Vec::new();
if let Ok(repos) = self.parse_settings_gradle_kts() {
repositories.extend(repos);
}
if let Ok(repos) = self.parse_settings_gradle() {
repositories.extend(repos);
}
if let Ok(repos) = self.parse_build_gradle_kts() {
repositories.extend(repos);
}
if let Ok(repos) = self.parse_build_gradle() {
repositories.extend(repos);
}
if repositories.is_empty() {
println!("⚠️ No repositories found in Gradle config, using defaults");
repositories = self.get_default_repositories();
}
repositories = self.deduplicate_repositories(repositories);
Ok(GradleConfig { repositories })
}
fn parse_settings_gradle_kts(&self) -> Result<Vec<Repository>> {
let path = self.project_path.join("settings.gradle.kts");
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)?;
self.extract_repositories_kotlin(&content)
}
fn parse_settings_gradle(&self) -> Result<Vec<Repository>> {
let path = self.project_path.join("settings.gradle");
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)?;
self.extract_repositories_groovy(&content)
}
fn parse_build_gradle_kts(&self) -> Result<Vec<Repository>> {
let path = self.project_path.join("build.gradle.kts");
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)?;
self.extract_repositories_kotlin(&content)
}
fn parse_build_gradle(&self) -> Result<Vec<Repository>> {
let path = self.project_path.join("build.gradle");
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)?;
self.extract_repositories_groovy(&content)
}
fn extract_repositories_kotlin(&self, content: &str) -> Result<Vec<Repository>> {
let mut repositories = Vec::new();
if content.contains("mavenCentral()") {
repositories.push(Repository {
name: "Maven Central".to_string(),
url: "https://repo1.maven.org/maven2".to_string(),
group_filters: Vec::new(),
});
}
if content.contains("google()") {
repositories.push(Repository {
name: "Google Maven".to_string(),
url: "https://dl.google.com/dl/android/maven2".to_string(),
group_filters: vec![
".*google.*".to_string(),
".*android.*".to_string(),
".*androidx.*".to_string(),
],
});
}
if content.contains("gradlePluginPortal()") {
repositories.push(Repository {
name: "Gradle Plugin Portal".to_string(),
url: "https://plugins.gradle.org/m2".to_string(),
group_filters: Vec::new(),
});
}
let maven_url_regex =
Regex::new(r#"maven\s*\{\s*url\s*=\s*uri\s*\(\s*["']([^"']+)["']\s*\)\s*\}"#)
.map_err(|e| GvcError::TomlParsing(format!("Regex error: {}", e)))?;
for cap in maven_url_regex.captures_iter(content) {
if let Some(url) = cap.get(1) {
repositories.push(Repository {
name: format!("Custom ({})", Self::shorten_url(url.as_str())),
url: url.as_str().to_string(),
group_filters: Vec::new(),
});
}
}
let maven_simple_regex = Regex::new(r#"maven\s*\(\s*["']([^"']+)["']\s*\)"#)
.map_err(|e| GvcError::TomlParsing(format!("Regex error: {}", e)))?;
for cap in maven_simple_regex.captures_iter(content) {
if let Some(url) = cap.get(1) {
repositories.push(Repository {
name: format!("Custom ({})", Self::shorten_url(url.as_str())),
url: url.as_str().to_string(),
group_filters: Vec::new(),
});
}
}
Ok(repositories)
}
fn extract_repositories_groovy(&self, content: &str) -> Result<Vec<Repository>> {
let mut repositories = Vec::new();
if content.contains("mavenCentral()") {
repositories.push(Repository {
name: "Maven Central".to_string(),
url: "https://repo1.maven.org/maven2".to_string(),
group_filters: Vec::new(),
});
}
if content.contains("google()") {
repositories.push(Repository {
name: "Google Maven".to_string(),
url: "https://dl.google.com/dl/android/maven2".to_string(),
group_filters: vec![
".*google.*".to_string(),
".*android.*".to_string(),
".*androidx.*".to_string(),
],
});
}
if content.contains("jcenter()") {
repositories.push(Repository {
name: "JCenter (Deprecated)".to_string(),
url: "https://jcenter.bintray.com".to_string(),
group_filters: Vec::new(),
});
}
let maven_url_regex = Regex::new(r#"maven\s*\{\s*url\s+['"]([^'"]+)['"]"#)
.map_err(|e| GvcError::TomlParsing(format!("Regex error: {}", e)))?;
for cap in maven_url_regex.captures_iter(content) {
if let Some(url) = cap.get(1) {
repositories.push(Repository {
name: format!("Custom ({})", Self::shorten_url(url.as_str())),
url: url.as_str().to_string(),
group_filters: Vec::new(),
});
}
}
let maven_url_equals_regex = Regex::new(r#"maven\s*\{\s*url\s*=\s*['"]([^'"]+)['"]"#)
.map_err(|e| GvcError::TomlParsing(format!("Regex error: {}", e)))?;
for cap in maven_url_equals_regex.captures_iter(content) {
if let Some(url) = cap.get(1) {
repositories.push(Repository {
name: format!("Custom ({})", Self::shorten_url(url.as_str())),
url: url.as_str().to_string(),
group_filters: Vec::new(),
});
}
}
Ok(repositories)
}
fn get_default_repositories(&self) -> Vec<Repository> {
vec![
Repository {
name: "Maven Central".to_string(),
url: "https://repo1.maven.org/maven2".to_string(),
group_filters: Vec::new(),
},
Repository {
name: "Google Maven".to_string(),
url: "https://dl.google.com/dl/android/maven2".to_string(),
group_filters: vec![
".*google.*".to_string(),
".*android.*".to_string(),
".*androidx.*".to_string(),
],
},
]
}
fn deduplicate_repositories(&self, repos: Vec<Repository>) -> Vec<Repository> {
let mut seen_urls = std::collections::HashSet::new();
let mut unique_repos = Vec::new();
for repo in repos {
let normalized_url = repo.url.trim_end_matches('/').to_string();
if seen_urls.insert(normalized_url.clone()) {
unique_repos.push(Repository {
name: repo.name,
url: normalized_url,
group_filters: repo.group_filters,
});
}
}
unique_repos
}
fn shorten_url(url: &str) -> String {
if let Some(domain_start) = url.find("://").map(|i| i + 3) {
let domain_part = &url[domain_start..];
if let Some(slash_pos) = domain_part.find('/') {
return domain_part[..slash_pos].to_string();
}
return domain_part.to_string();
}
url.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_kotlin_dsl() {
let content = r#"
repositories {
mavenCentral()
google()
maven { url = uri("https://jitpack.io") }
}
"#;
let parser = GradleConfigParser::new(".");
let repos = parser.extract_repositories_kotlin(content).unwrap();
assert_eq!(repos.len(), 3);
assert!(
repos
.iter()
.any(|r| r.url == "https://repo1.maven.org/maven2")
);
assert!(repos.iter().any(|r| r.url == "https://jitpack.io"));
}
#[test]
fn test_extract_groovy_dsl() {
let content = r#"
repositories {
mavenCentral()
google()
maven { url 'https://jitpack.io' }
}
"#;
let parser = GradleConfigParser::new(".");
let repos = parser.extract_repositories_groovy(content).unwrap();
assert_eq!(repos.len(), 3);
assert!(repos.iter().any(|r| r.url == "https://jitpack.io"));
}
#[test]
fn test_deduplicate() {
let repos = vec![
Repository {
name: "Maven Central".to_string(),
url: "https://repo1.maven.org/maven2".to_string(),
group_filters: Vec::new(),
},
Repository {
name: "Maven Central".to_string(),
url: "https://repo1.maven.org/maven2/".to_string(), group_filters: Vec::new(),
},
Repository {
name: "Google".to_string(),
url: "https://dl.google.com/dl/android/maven2".to_string(),
group_filters: Vec::new(),
},
];
let parser = GradleConfigParser::new(".");
let unique = parser.deduplicate_repositories(repos);
assert_eq!(unique.len(), 2);
}
}