use tracing::{debug, info};
pub fn sweep_orphans(
roots: &[std::path::PathBuf],
keep: &std::collections::HashSet<std::path::PathBuf>,
) -> anyhow::Result<usize> {
fn is_alef_owned(path: &std::path::Path) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
crate::core::hash::extract_hash(&content).is_some()
}
let mut removed = 0usize;
let mut touched_dirs: std::collections::BTreeSet<std::path::PathBuf> = std::collections::BTreeSet::new();
for root in roots {
if !root.exists() {
continue;
}
let mut stack = vec![root.clone()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(it) => it,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
name,
".git"
| "target"
| "node_modules"
| "vendor"
| "_build"
| "deps"
| ".venv"
| "venv"
| "build"
| "dist"
| "Pods"
) {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
if keep.contains(&path) {
continue;
}
if !is_alef_owned(&path) {
continue;
}
if let Err(err) = std::fs::remove_file(&path) {
debug!(" sweep skip (remove failed): {} ({err})", path.display());
continue;
}
debug!(" swept orphan: {}", path.display());
if let Some(parent) = path.parent() {
touched_dirs.insert(parent.to_path_buf());
}
removed += 1;
}
}
}
let mut dirs: Vec<_> = touched_dirs.into_iter().collect();
dirs.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
for dir in dirs {
let _ = std::fs::remove_dir(&dir);
}
if removed > 0 {
info!("Swept {removed} orphan generated file(s)");
}
Ok(removed)
}
pub fn collect_alef_headered_paths(root: &std::path::Path) -> std::collections::HashSet<std::path::PathBuf> {
fn is_alef_owned(path: &std::path::Path) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
crate::core::hash::extract_hash(&content).is_some()
}
let mut paths = std::collections::HashSet::new();
if !root.exists() {
return paths;
}
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(it) => it,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(ft) = entry.file_type() else { continue };
if ft.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
name,
".git"
| "target"
| "node_modules"
| "vendor"
| "_build"
| "deps"
| ".venv"
| "venv"
| "build"
| "dist"
| "Pods"
) {
continue;
}
stack.push(path);
} else if ft.is_file() && is_alef_owned(&path) {
paths.insert(path);
}
}
}
paths
}