use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver};
use std::thread;
use tokei::{Config as TokeiConfig, Languages};
use crate::domain::error::Result;
use crate::domain::stats::{CodeEntry, CodeStats, GitStats, LangCount};
use crate::storage::git_client::GitClient;
use crate::storage::stats_cache;
use crate::util::diskusage;
pub use crate::storage::stats_cache::StatsCache;
pub fn load_cache(path: &Path) -> StatsCache {
stats_cache::load(path)
}
pub fn save_cache(path: &Path, cache: &StatsCache) -> Result<()> {
stats_cache::save(path, cache)
}
pub enum CodeUpdate {
Started {
path: PathBuf,
},
Done {
path: PathBuf,
stats: Box<CodeEntry>,
},
}
pub enum GitStatsUpdate {
Started {
path: PathBuf,
},
Done {
path: PathBuf,
stats: GitStats,
},
}
pub fn spawn_code_stats(
paths: Vec<PathBuf>,
exclude_dirs: Vec<String>,
) -> Receiver<CodeUpdate> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for path in paths {
let started = CodeUpdate::Started { path: path.clone() };
if tx.send(started).is_err() {
return;
}
let stats = Box::new(CodeEntry {
code: count_lines(&path, &exclude_dirs),
disk: diskusage::measure(&path, &exclude_dirs),
});
if tx.send(CodeUpdate::Done { path, stats }).is_err() {
return;
}
}
});
rx
}
pub fn spawn_git_stats(
client: Arc<dyn GitClient>,
paths: Vec<PathBuf>,
) -> Receiver<GitStatsUpdate> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for path in paths {
let started = GitStatsUpdate::Started { path: path.clone() };
if tx.send(started).is_err() {
return;
}
let stats = client.stats(&path);
if tx.send(GitStatsUpdate::Done { path, stats }).is_err() {
return;
}
}
});
rx
}
fn count_lines(path: &PathBuf, exclude_dirs: &[String]) -> CodeStats {
let globs = exclude_globs(exclude_dirs);
let ignored: Vec<&str> = globs.iter().map(String::as_str).collect();
let config = TokeiConfig {
no_ignore_parent: Some(true),
..TokeiConfig::default()
};
let mut languages = Languages::new();
languages.get_statistics(std::slice::from_ref(path), &ignored, &config);
let counts = languages
.into_iter()
.map(|(kind, language)| {
(
kind.name().to_string(),
LangCount {
code: language.code,
comments: language.comments,
blanks: language.blanks,
files: language.reports.len(),
},
)
})
.filter(|(_, count)| count.files > 0)
.collect();
CodeStats { languages: counts }
}
fn exclude_globs(exclude_dirs: &[String]) -> Vec<String> {
exclude_dirs
.iter()
.filter(|dir| !dir.is_empty())
.map(|dir| format!("{dir}*/"))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exclude_globs_keep_the_prefix_rule_and_target_directories() {
assert_eq!(
exclude_globs(&["target".to_string(), "node_modules".to_string()]),
vec!["target*/".to_string(), "node_modules*/".to_string()]
);
assert!(exclude_globs(&[String::new()]).is_empty());
}
}