use tokei::{CodeStats, Config, LanguageType};
use super::super::FetchError;
use super::super::git::fail;
const FILE_SIZE_CAP: u64 = 1024 * 1024;
const MAX_FILES_SCANNED: usize = 5_000;
pub const EXCLUDED_DIRS: &[&str] = &[
"node_modules",
"vendor",
"third_party",
"Pods",
"target",
"dist",
"build",
"out",
"_build",
"_site",
"DerivedData",
".next",
".nuxt",
".svelte-kit",
".turbo",
".cache",
"coverage",
"htmlcov",
"__snapshots__",
"__pycache__",
".git",
];
pub const EXCLUDED_FILENAMES: &[&str] = &[
"Cargo.lock",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lock",
"bun.lockb",
"Gemfile.lock",
"composer.lock",
"poetry.lock",
"Pipfile.lock",
"uv.lock",
"go.sum",
"flake.lock",
"mix.lock",
"Podfile.lock",
];
pub fn for_each_tracked_path<F>(repo: &gix::Repository, mut visit: F) -> Result<(), FetchError>
where
F: FnMut(&str),
{
let index = repo.index_or_load_from_head_or_empty().map_err(fail)?;
let mut scanned = 0usize;
for entry in index.entries() {
if scanned >= MAX_FILES_SCANNED {
break;
}
if !is_regular_file(entry.mode) {
continue;
}
let Ok(path_str) = std::str::from_utf8(entry.path(&index)) else {
continue;
};
if is_in_excluded_dir(path_str) || is_excluded_filename(path_str) {
continue;
}
scanned += 1;
visit(path_str);
}
Ok(())
}
pub fn for_each_tracked_file<F>(repo: &gix::Repository, mut visit: F) -> Result<(), FetchError>
where
F: FnMut(&str, &[u8]),
{
let Some(workdir) = repo.workdir().map(|p| p.to_path_buf()) else {
return Ok(());
};
let index = repo.index_or_load_from_head_or_empty().map_err(fail)?;
let mut scanned = 0usize;
for entry in index.entries() {
if scanned >= MAX_FILES_SCANNED {
break;
}
if !is_regular_file(entry.mode) {
continue;
}
let Ok(path_str) = std::str::from_utf8(entry.path(&index)) else {
continue;
};
if is_in_excluded_dir(path_str) || is_excluded_filename(path_str) {
continue;
}
scanned += 1;
let abs = workdir.join(path_str);
let Ok(metadata) = std::fs::metadata(&abs) else {
continue;
};
if !metadata.is_file() || metadata.len() > FILE_SIZE_CAP {
continue;
}
let Ok(bytes) = std::fs::read(&abs) else {
continue;
};
if looks_binary(&bytes) {
continue;
}
visit(path_str, &bytes);
}
Ok(())
}
pub fn classify_with_lang(path: &str, cfg: &Config) -> Option<(&'static str, LanguageType)> {
if let Some(forced) = special_basename(path) {
return Some(forced);
}
let lang = LanguageType::from_path(path, cfg)?;
Some((canonical_name(lang.name()), lang))
}
pub fn classify_path(path: &str) -> Option<&'static str> {
classify_with_lang(path, &Config::default()).map(|(name, _)| name)
}
fn special_basename(path: &str) -> Option<(&'static str, LanguageType)> {
let basename = path.rsplit('/').next().unwrap_or(path);
match basename {
"Gemfile" | "Brewfile" => Some(("Ruby", LanguageType::Ruby)),
_ => None,
}
}
fn canonical_name(name: &'static str) -> &'static str {
match name {
"TSX" => "TypeScript",
"JSX" => "JavaScript",
_ => name,
}
}
pub fn for_each_tokei_stat<F>(repo: &gix::Repository, mut visit: F) -> Result<(), FetchError>
where
F: FnMut(&str, &'static str, &CodeStats),
{
let cfg = Config::default();
for_each_tracked_file(repo, |path, bytes| {
if let Some((name, lang)) = classify_with_lang(path, &cfg) {
let stats = lang.parse_from_slice(bytes, &cfg);
visit(path, name, &stats);
}
})
}
fn is_regular_file(mode: gix::index::entry::Mode) -> bool {
use gix::index::entry::Mode;
matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE)
}
pub fn is_in_excluded_dir(path: &str) -> bool {
path.split('/').any(|seg| EXCLUDED_DIRS.contains(&seg))
}
pub fn is_excluded_filename(path: &str) -> bool {
let basename = path.rsplit('/').next().unwrap_or(path);
EXCLUDED_FILENAMES.contains(&basename)
}
pub fn looks_binary(bytes: &[u8]) -> bool {
let head = &bytes[..bytes.len().min(8192)];
head.contains(&0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn excluded_dir_check_only_matches_segments() {
assert!(is_in_excluded_dir("vendor/foo.go"));
assert!(is_in_excluded_dir("src/vendor/foo.go"));
assert!(is_in_excluded_dir("a/b/node_modules/c"));
assert!(!is_in_excluded_dir("myvendor/foo.go"));
assert!(!is_in_excluded_dir("src/vendor.go"));
assert!(!is_in_excluded_dir("README.md"));
}
#[test]
fn excluded_filename_matches_basename_anywhere_in_tree() {
assert!(is_excluded_filename("Cargo.lock"));
assert!(is_excluded_filename("crates/foo/Cargo.lock"));
assert!(is_excluded_filename("frontend/package-lock.json"));
assert!(is_excluded_filename("vendor/foo/go.sum"));
assert!(!is_excluded_filename("Cargo.lock.bak"));
assert!(!is_excluded_filename("not-package-lock.json"));
assert!(!is_excluded_filename("README.md"));
}
#[test]
fn looks_binary_detects_nul_byte_in_head() {
assert!(looks_binary(b"\x00\x01\x02"));
assert!(looks_binary(b"text\x00more"));
assert!(!looks_binary(b"plain ascii"));
assert!(!looks_binary(b""));
}
}