use std::cell::RefCell;
use std::fs::File;
use std::io::{self, Cursor, Read};
use std::path::{Path, PathBuf};
use git2::{Oid, Repository};
use crate::analyzer::reader::Reader;
use crate::config::LanguageIndex;
use crate::logger::{logger, Level, Log, Verbosity};
#[derive(Clone)]
pub enum AnalysisSource {
Disk { root: PathBuf },
Git { repo_path: PathBuf, commit_id: Oid },
}
impl AnalysisSource {
fn reader_for(&self, rel_path: &Path) -> io::Result<Reader> {
match self {
AnalysisSource::Disk { root } => {
let full = root.join(rel_path);
let file = File::open(&full)?;
let size = file.metadata()?.len();
Ok(Reader::Disk { file, size })
}
AnalysisSource::Git {
repo_path,
commit_id,
} => {
with_thread_repo(repo_path, |repo| {
let commit_obj = repo.find_commit(*commit_id).map_err(io::Error::other)?;
let tree = commit_obj.tree().map_err(io::Error::other)?;
let entry = tree.get_path(rel_path).map_err(|_| {
io::Error::new(io::ErrorKind::NotFound, "The file isn't in the commit")
})?;
let obj = entry.to_object(repo).map_err(io::Error::other)?;
let blob = obj.as_blob().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "It's Not a file")
})?;
let data = blob.content().to_vec();
Ok(Reader::Commit {
cursor: Cursor::new(data),
})
})
}
}
}
pub fn analyze_file(
&self,
path: &Path,
index: &LanguageIndex,
verbose: &u8,
) -> Option<(String, u32, u64)> {
let mut reader = self
.reader_for(path)
.map_err(|e| {
logger(
Log::new(
Level::Warning,
format!("Can't read {}:{}", path.display(), e),
None,
),
None,
);
})
.ok()?;
analyze_reader(&mut reader, path, index, verbose)
}
}
thread_local! {
static REPO_CACHE: RefCell<Option<Repository>> = const { RefCell::new(None) };
}
const CHUNK: usize = 64 * 1024;
fn with_thread_repo<T>(
repo_path: &Path,
f: impl FnOnce(&Repository) -> io::Result<T>,
) -> io::Result<T> {
REPO_CACHE.with(|cell| {
let mut opt = cell.borrow_mut();
if opt.is_none() {
let repo = Repository::open(repo_path).map_err(io::Error::other)?;
*opt = Some(repo);
}
f(opt.as_ref().unwrap())
})
}
fn analyze_reader(
reader: &mut Reader,
path: &Path,
index: &LanguageIndex,
verbose: &u8,
) -> Option<(String, u32, u64)> {
let mut buf = [0u8; CHUNK];
let mut newline_count = 0u32;
let mut last_byte = None;
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
newline_count += memchr::memchr_iter(b'\n', &buf[..n]).count() as u32;
last_byte = Some(buf[n - 1]);
}
Err(_) => return None,
}
}
let lines = match last_byte {
None => 0,
Some(b) if b != b'\n' => newline_count + 1,
Some(_) => newline_count,
};
let lang_entry = index.detect(path)?;
let lang_id = lang_entry.id.clone();
logger(
Log::new(
Level::Info,
format!(" {} -> {}", path.display(), lang_entry.name),
Some(Verbosity::Debug),
),
Some(Verbosity::from_u8(verbose)),
);
let bytes = reader.size();
Some((lang_id, lines, bytes))
}