use anyhow::{Result, bail};
use console::style;
use std::path::{Path, PathBuf};
use crate::config;
pub fn execute(yes: bool, pattern: Option<&str>) -> Result<()> {
let project = std::env::current_dir()
.ok()
.and_then(|cwd| config::find_config(&cwd))
.map(|cfg_path| config::project_dir(&cfg_path));
let maven_cache = config::maven_cache_dir();
let pom_cache = config::pom_cache_dir();
match pattern {
Some(pat) => clean_pattern(&maven_cache, &pom_cache, project.as_deref(), pat, yes),
None => clean_all(&maven_cache, &pom_cache, project.as_deref(), yes),
}
}
fn clean_all(
maven_cache: &Path,
pom_cache: &Path,
project: Option<&Path>,
yes: bool,
) -> Result<()> {
let size = config::dir_size(maven_cache) + config::dir_size(pom_cache);
let resolved = project
.map(config::resolved_cache_path)
.filter(|p| p.exists());
if size == 0 && resolved.is_none() {
println!(" {} No cache to clean", style("✓").green());
return Ok(());
}
if !yes {
let confirm = dialoguer::Confirm::new()
.with_prompt(format!(
" Delete entire dependency cache ({})?",
config::format_size(size)
))
.default(false)
.interact()?;
if !confirm {
println!(" {} Cancelled", style("!").yellow());
return Ok(());
}
}
if maven_cache.exists() {
std::fs::remove_dir_all(maven_cache)?;
println!(" {} Removed {}", style("✓").green(), maven_cache.display());
}
if pom_cache.exists() {
std::fs::remove_dir_all(pom_cache)?;
println!(" {} Removed {}", style("✓").green(), pom_cache.display());
}
if let Some(path) = resolved {
std::fs::remove_file(&path)?;
println!(" {} Removed {}", style("✓").green(), path.display());
}
println!(
" {} Cache clean complete ({} freed)",
style("✓").green(),
config::format_size(size)
);
Ok(())
}
#[derive(Debug)]
enum CachePattern {
ArtifactAnyGroup(String),
GroupWildcard(String),
Ga { group: String, artifact: String },
Gav { group: String, artifact: String, version: String },
}
impl CachePattern {
fn parse(raw: &str) -> Result<Self> {
let parts: Vec<&str> = raw.split(':').collect();
match parts.as_slice() {
[a] if !a.is_empty() => Ok(Self::ArtifactAnyGroup((*a).to_string())),
[g, "*"] if !g.is_empty() => Ok(Self::GroupWildcard((*g).to_string())),
[g, a] if !g.is_empty() && !a.is_empty() => Ok(Self::Ga {
group: (*g).to_string(),
artifact: (*a).to_string(),
}),
[g, a, v] if !g.is_empty() && !a.is_empty() && !v.is_empty() => Ok(Self::Gav {
group: (*g).to_string(),
artifact: (*a).to_string(),
version: (*v).to_string(),
}),
_ => bail!(
"Invalid pattern '{}'. Expected: <artifact>, <group>:*, <group>:<artifact>, or <group>:<artifact>:<version>",
raw
),
}
}
}
fn match_cache<F>(cache: &Path, pat: &CachePattern, gav_leaf: F) -> Vec<PathBuf>
where
F: Fn(&Path, &str, &str, &str) -> PathBuf,
{
let mut matches = Vec::new();
match pat {
CachePattern::ArtifactAnyGroup(artifact) => {
let Ok(groups) = std::fs::read_dir(cache) else { return matches };
for entry in groups.flatten() {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
let artifact_dir = entry.path().join(artifact);
if artifact_dir.is_dir() {
matches.push(artifact_dir);
}
}
}
CachePattern::GroupWildcard(group) => {
let p = cache.join(group);
if p.is_dir() {
matches.push(p);
}
}
CachePattern::Ga { group, artifact } => {
let p = cache.join(group).join(artifact);
if p.is_dir() {
matches.push(p);
}
}
CachePattern::Gav { group, artifact, version } => {
let p = gav_leaf(cache, group, artifact, version);
if p.exists() {
matches.push(p);
}
}
}
matches
}
fn match_maven_cache(cache: &Path, pat: &CachePattern) -> Vec<PathBuf> {
match_cache(cache, pat, |root, g, a, v| root.join(g).join(a).join(v))
}
fn match_pom_cache(cache: &Path, pat: &CachePattern) -> Vec<PathBuf> {
match_cache(cache, pat, |root, g, a, v| {
root.join(g).join(a).join(format!("{}.json", v))
})
}
fn clean_pattern(
maven_cache: &Path,
pom_cache: &Path,
project: Option<&Path>,
pattern: &str,
yes: bool,
) -> Result<()> {
let pat = CachePattern::parse(pattern)?;
let mut matches = match_maven_cache(maven_cache, &pat);
matches.extend(match_pom_cache(pom_cache, &pat));
if matches.is_empty() {
println!(
" {} No cache entries matched '{}'",
style("!").yellow(),
pattern
);
return Ok(());
}
let total_size: u64 = matches.iter().map(|p| path_size(p)).sum();
if !yes {
println!(
" {} Matched {} entr{} for '{}':",
style("→").cyan(),
matches.len(),
if matches.len() == 1 { "y" } else { "ies" },
pattern
);
for p in &matches {
println!(" {}", p.display());
}
let confirm = dialoguer::Confirm::new()
.with_prompt(format!(
" Delete matched entries ({})?",
config::format_size(total_size)
))
.default(false)
.interact()?;
if !confirm {
println!(" {} Cancelled", style("!").yellow());
return Ok(());
}
}
for p in &matches {
remove_path(p)?;
println!(" {} Removed {}", style("✓").green(), p.display());
}
if let Some(p) = project {
let path = config::resolved_cache_path(p);
if path.exists() {
std::fs::remove_file(&path)?;
println!(" {} Invalidated {}", style("✓").green(), path.display());
}
}
println!(
" {} Cache clean complete ({} freed)",
style("✓").green(),
config::format_size(total_size)
);
Ok(())
}
fn remove_path(p: &Path) -> Result<()> {
match std::fs::metadata(p) {
Ok(m) if m.is_dir() => std::fs::remove_dir_all(p)?,
Ok(_) => std::fs::remove_file(p)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
Ok(())
}
fn path_size(p: &Path) -> u64 {
match std::fs::metadata(p) {
Ok(m) if m.is_file() => m.len(),
Ok(_) => config::dir_size(p),
Err(_) => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_artifact_any_group() {
match CachePattern::parse("theme-service").unwrap() {
CachePattern::ArtifactAnyGroup(a) => assert_eq!(a, "theme-service"),
other => panic!("expected ArtifactAnyGroup, got {:?}", other),
}
}
#[test]
fn parse_group_wildcard() {
match CachePattern::parse("com.summer.jarvis:*").unwrap() {
CachePattern::GroupWildcard(g) => assert_eq!(g, "com.summer.jarvis"),
other => panic!("expected GroupWildcard, got {:?}", other),
}
}
#[test]
fn parse_ga_exact() {
match CachePattern::parse("com.summer.jarvis:theme-service").unwrap() {
CachePattern::Ga { group, artifact } => {
assert_eq!(group, "com.summer.jarvis");
assert_eq!(artifact, "theme-service");
}
other => panic!("expected Ga, got {:?}", other),
}
}
#[test]
fn parse_gav_exact() {
match CachePattern::parse("com.summer.jarvis:theme-service:4.0.12").unwrap() {
CachePattern::Gav { group, artifact, version } => {
assert_eq!(group, "com.summer.jarvis");
assert_eq!(artifact, "theme-service");
assert_eq!(version, "4.0.12");
}
other => panic!("expected Gav, got {:?}", other),
}
}
#[test]
fn parse_invalid_empty() {
assert!(CachePattern::parse("").is_err());
assert!(CachePattern::parse(":").is_err());
assert!(CachePattern::parse("a:").is_err());
assert!(CachePattern::parse(":b").is_err());
}
#[test]
fn parse_invalid_too_many_parts() {
assert!(CachePattern::parse("a:b:c:d").is_err());
}
#[test]
fn match_maven_artifact_fuzzy() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path();
std::fs::create_dir_all(cache.join("com.example").join("foo").join("1.0")).unwrap();
std::fs::create_dir_all(cache.join("org.other").join("foo").join("2.0")).unwrap();
std::fs::create_dir_all(cache.join("com.example").join("bar").join("1.0")).unwrap();
let matches = match_maven_cache(cache, &CachePattern::ArtifactAnyGroup("foo".to_string()));
assert_eq!(matches.len(), 2);
}
#[test]
fn match_maven_gav_exact() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path();
std::fs::create_dir_all(cache.join("com.example").join("foo").join("1.0")).unwrap();
std::fs::create_dir_all(cache.join("com.example").join("foo").join("2.0")).unwrap();
let matches = match_maven_cache(
cache,
&CachePattern::Gav {
group: "com.example".to_string(),
artifact: "foo".to_string(),
version: "1.0".to_string(),
},
);
assert_eq!(matches.len(), 1);
assert!(matches[0].ends_with("1.0"));
}
#[test]
fn match_pom_gav_is_file() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path();
let group_dir = cache.join("com.example").join("foo");
std::fs::create_dir_all(&group_dir).unwrap();
std::fs::write(group_dir.join("1.0.json"), "[]").unwrap();
let matches = match_pom_cache(
cache,
&CachePattern::Gav {
group: "com.example".to_string(),
artifact: "foo".to_string(),
version: "1.0".to_string(),
},
);
assert_eq!(matches.len(), 1);
assert!(matches[0].is_file());
}
}