use anyhow::{Context, Result};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::fs;
use std::path::{Path, PathBuf};
pub const IGNORE_FILENAME: &str = ".lantern-ignore";
pub const DEFAULT_PATTERNS: &[&str] = &[
".git/",
"target/",
"node_modules/",
".hermes/",
"__pycache__/",
".venv/",
"vendor/",
];
pub struct IgnoreRules {
matcher: Gitignore,
bypass: bool,
}
impl IgnoreRules {
pub fn disabled() -> Self {
Self {
matcher: Gitignore::empty(),
bypass: true,
}
}
pub fn load(ingest_path: &Path) -> Result<Self> {
let start_dir = start_directory(ingest_path);
let start_abs = fs::canonicalize(&start_dir).unwrap_or(start_dir);
let found = find_ignore_file(&start_abs);
let matcher = match found {
Some((file, root)) => {
let mut builder = GitignoreBuilder::new(&root);
if let Some(err) = builder.add(&file) {
return Err(
anyhow::Error::from(err).context(format!("parse {}", file.display()))
);
}
builder
.build()
.with_context(|| format!("compile rules from {}", file.display()))?
}
None => {
let mut builder = GitignoreBuilder::new(&start_abs);
for pat in DEFAULT_PATTERNS {
builder
.add_line(None, pat)
.with_context(|| format!("default pattern: {pat}"))?;
}
builder.build().context("compile default ignore rules")?
}
};
Ok(Self {
matcher,
bypass: false,
})
}
pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
if self.bypass {
return false;
}
self.matcher
.matched_path_or_any_parents(path, is_dir)
.is_ignore()
}
}
fn start_directory(ingest_path: &Path) -> PathBuf {
match fs::metadata(ingest_path) {
Ok(m) if m.is_dir() => ingest_path.to_path_buf(),
_ => ingest_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(".")),
}
}
fn find_ignore_file(start: &Path) -> Option<(PathBuf, PathBuf)> {
let mut cursor: &Path = start;
loop {
let candidate = cursor.join(IGNORE_FILENAME);
if candidate.is_file() {
return Some((candidate, cursor.to_path_buf()));
}
match cursor.parent() {
Some(p) if p != cursor => cursor = p,
_ => return None,
}
}
}