use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use ignore::WalkBuilder;
use ignore::overrides::OverrideBuilder;
const BUILTIN_IGNORES: &[&str] = &["node_modules", "target", "dist", "build", ".hg", ".svn"];
pub struct WalkResult {
pub files: Vec<PathBuf>,
pub errors: Vec<String>,
}
pub fn walk_hsml_files(dir: &Path, ignore_patterns: &[String]) -> Result<WalkResult, String> {
let mut builder = WalkBuilder::new(dir);
builder
.add_custom_ignore_filename(".hsmlignore")
.require_git(false)
.follow_links(false);
if !ignore_patterns.is_empty() {
let mut overrides = OverrideBuilder::new(dir);
for pattern in ignore_patterns {
overrides
.add(&format!("!{pattern}"))
.map_err(|e| format!("Invalid ignore pattern '{pattern}': {e}"))?;
}
let overrides = overrides
.build()
.map_err(|e| format!("Failed to build ignore patterns: {e}"))?;
builder.overrides(overrides);
}
let reincluded = load_reinclude_patterns(dir);
let mut files = Vec::new();
let mut errors = Vec::new();
for entry in builder.build() {
match entry {
Ok(entry) => {
let is_file = entry.file_type().map(|ft| ft.is_file()).unwrap_or(false);
let path = entry.path();
if is_file
&& path.extension().is_some_and(|ext| ext == "hsml")
&& !is_builtin_ignored(dir, path, &reincluded)
{
files.push(path.to_path_buf());
}
}
Err(e) => {
errors.push(format!("{e}"));
}
}
}
Ok(WalkResult { files, errors })
}
fn load_reinclude_patterns(dir: &Path) -> HashSet<String> {
let Ok(content) = std::fs::read_to_string(dir.join(".hsmlignore")) else {
return HashSet::new();
};
content
.lines()
.filter_map(|line| line.strip_prefix('!'))
.filter_map(|pattern| {
let cleaned = pattern.trim_start_matches('/').trim_end_matches('/');
Path::new(cleaned)
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
})
.collect()
}
fn is_builtin_ignored(root: &Path, path: &Path, reincluded: &HashSet<String>) -> bool {
let Ok(rel) = path.strip_prefix(root) else {
return false;
};
rel.ancestors().any(|ancestor| {
ancestor.file_name().is_some_and(|name| {
BUILTIN_IGNORES
.iter()
.any(|&ig| name == OsStr::new(ig) && !reincluded.contains(ig))
})
})
}