paline 0.5.0

一个用 Rust 编写的代码行数统计命令行工具。递归扫描目录,按编程语言统计行数,并以彩色横向条形图在终端中可视化展示。
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 {
    /// 创建用于读取一个文件的 Reader
    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,
            } => {
                // 使用线程本地的 repo 缓存
                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,
    };

    // 语言检测,使用 LanguageIndex
    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))
}