use std::{collections::BTreeSet, fs, path::Path};
use crate::{GitError, Result, gitignore::IgnoreStack};
#[derive(Default)]
pub(crate) struct WalkCounts {
pub untracked: u64,
pub ignored: u64,
pub visited: u64,
pub untracked_samples: Vec<Vec<u8>>,
pub ignored_samples: Vec<Vec<u8>>,
}
const SAMPLE_LIMIT: usize = 32;
pub(crate) fn count_untracked(
root: &Path,
indexed: &BTreeSet<Vec<u8>>,
ignore: &IgnoreStack,
limit: usize,
) -> Result<WalkCounts> {
let mut counts = WalkCounts::default();
walk(root, root, "", indexed, ignore, limit, &mut counts)?;
Ok(counts)
}
fn walk(
root: &Path,
dir: &Path,
relative: &str,
indexed: &BTreeSet<Vec<u8>>,
parent_ignore: &IgnoreStack,
limit: usize,
counts: &mut WalkCounts,
) -> Result<()> {
if counts.visited >= limit as u64 {
return Err(GitError::LimitExceeded {
resource: "worktree entries",
limit,
});
}
let mut ignore = parent_ignore.clone();
if let Ok(text) = fs::read_to_string(dir.join(".gitignore")) {
ignore.push_file(relative, &text);
}
for entry in fs::read_dir(dir)? {
classify_dirent(root, indexed, &ignore, limit, counts, relative, &entry?)?;
}
Ok(())
}
fn classify_dirent(
root: &Path,
indexed: &BTreeSet<Vec<u8>>,
ignore: &IgnoreStack,
limit: usize,
counts: &mut WalkCounts,
relative: &str,
entry: &fs::DirEntry,
) -> Result<()> {
counts.visited += 1;
if counts.visited > limit as u64 {
return Err(GitError::LimitExceeded {
resource: "worktree entries",
limit,
});
}
let name = entry.file_name();
if name == ".git" {
return Ok(());
}
let path = entry.path();
let metadata = fs::symlink_metadata(&path)?;
if metadata.file_type().is_symlink() {
return Ok(());
}
let child_rel = join_relative(relative, &name);
let is_dir = metadata.is_dir();
let bytes = child_rel.as_bytes().to_vec();
if is_dir && is_tracked_dir(indexed, &bytes) {
return walk(root, &path, &child_rel, indexed, ignore, limit, counts);
}
if indexed.contains(&bytes) {
return Ok(());
}
if ignore.is_ignored(&child_rel, is_dir) {
counts.ignored += 1;
push_sample(&mut counts.ignored_samples, bytes);
return Ok(());
}
counts.untracked += 1;
push_sample(&mut counts.untracked_samples, bytes);
if is_dir {
walk(root, &path, &child_rel, indexed, ignore, limit, counts)?;
}
Ok(())
}
fn join_relative(prefix: &str, name: &std::ffi::OsStr) -> String {
let name = name.to_string_lossy();
if prefix.is_empty() {
name.into_owned()
} else {
format!("{prefix}/{name}")
}
}
fn is_tracked_dir(indexed: &BTreeSet<Vec<u8>>, path: &[u8]) -> bool {
let mut prefix = path.to_vec();
prefix.push(b'/');
indexed
.range(prefix.clone()..)
.next()
.is_some_and(|candidate| candidate.starts_with(&prefix))
}
fn push_sample(samples: &mut Vec<Vec<u8>>, path: Vec<u8>) {
if samples.len() < SAMPLE_LIMIT {
samples.push(path);
}
}