use crate::prelude::*;
use std::collections::HashSet;
#[derive(Debug, Default, Clone)]
pub struct Ignored {
entries: HashSet<PathBuf>,
repositories: Vec<PathBuf>,
}
impl Ignored {
pub fn discover(root: &Path) -> Self {
let mut found = Self::default();
let mut pending: Vec<PathBuf> = Vec::new();
let mut seen: HashSet<PathBuf> = HashSet::new();
if let Ok(owner) = crate::git_backend::repo_root(root) {
pending.push(owner);
}
while let Some(repo) = pending.pop() {
if !seen.insert(repo.clone()) {
continue;
}
found.absorb(&repo);
found.repositories.push(repo.clone());
let search_from = if repo.starts_with(root) { &repo } else { root };
for nested in found.nested_repositories(search_from, &seen) {
pending.push(nested);
}
}
found
}
fn absorb(&mut self, repo: &Path) {
let listed = crate::git_backend::git_command()
.arg("-C")
.arg(repo)
.args([
"ls-files",
"-z",
"--others",
"--ignored",
"--exclude-standard",
"--directory",
])
.output();
let Ok(listed) = listed else {
return;
};
if !listed.status.success() {
return;
}
for raw in listed.stdout.split(|byte| *byte == 0) {
if raw.is_empty() {
continue;
}
let relative = String::from_utf8_lossy(raw);
let trimmed = relative.trim_end_matches('/');
if trimmed.is_empty() {
continue;
}
self.entries.insert(repo.join(trimmed));
}
}
fn nested_repositories(&self, from: &Path, seen: &HashSet<PathBuf>) -> Vec<PathBuf> {
let mut found = Vec::new();
let walker = walkdir::WalkDir::new(from)
.follow_links(false)
.into_iter()
.filter_entry(|entry| {
let path = entry.path();
if path == from {
return true;
}
if !entry.file_type().is_dir() {
return false;
}
path.file_name().is_none_or(|name| name != ".git") && !self.contains(path)
});
for entry in walker.flatten() {
let path = entry.path();
if path == from || !entry.file_type().is_dir() {
continue;
}
if path.join(".git").exists() && !seen.contains(path) {
found.push(path.to_path_buf());
}
}
found
}
pub fn contains(&self, path: &Path) -> bool {
if self.entries.is_empty() {
return false;
}
let mut current = Some(path);
while let Some(at) = current {
if self.entries.contains(at) {
return true;
}
current = at.parent();
}
false
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn repositories(&self) -> &[PathBuf] {
&self.repositories
}
}
#[cfg(test)]
#[path = "git_ignored_tests.rs"]
mod git_ignored_tests;