paline 0.5.0

一个用 Rust 编写的代码行数统计命令行工具。递归扫描目录,按编程语言统计行数,并以彩色横向条形图在终端中可视化展示。
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use git2::{Commit, Repository, Tree};
use ignore::WalkBuilder;

use crate::analyzer::{parallel::CollectorBuilder, source::AnalysisSource};

pub enum Project<'a> {
    Disk {
        root: &'a Path,
    },
    Commit {
        repo: &'a Repository,
        commit: &'a Commit<'a>,
    },
}

impl<'a> Project<'a> {
    /// 获取所有待分析文件的路径
    pub fn file_paths(&self) -> Vec<PathBuf> {
        match self {
            Project::Disk { root } => {
                // 将根目录转换为绝对路径,确保遍历和后续拼接一致
                let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
                let collector: Arc<Mutex<Vec<PathBuf>>> = Arc::new(Mutex::new(Vec::new()));
                let builder = WalkBuilder::new(&root_abs); // 使用绝对路径
                builder.build_parallel().visit(&mut CollectorBuilder {
                    collector: collector.clone(),
                });
                let mut files = collector.lock().unwrap();
                files.drain(..).collect()
            }
            Project::Commit { repo, commit } => {
                let tree = commit.tree().expect("Failed to get commit hash");
                collect_commit_files(repo, &tree, Path::new(""))
            }
        }
    }

    /// 创建线程安全的分析源
    pub fn to_analysis_source(&self) -> AnalysisSource {
        match self {
            Project::Disk { root } => {
                // 同样转换为绝对路径存储
                let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
                AnalysisSource::Disk { root: root_abs }
            }
            Project::Commit { repo, commit } => {
                let repo_path = repo
                    .path()
                    .parent()
                    .unwrap_or_else(|| repo.path())
                    .to_path_buf();
                AnalysisSource::Git {
                    repo_path,
                    commit_id: commit.id(),
                }
            }
        }
    }
}

fn collect_commit_files(repo: &Repository, tree: &Tree, prefix: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    let mut stack: Vec<(Tree, PathBuf)> = vec![(tree.clone(), prefix.to_path_buf())];

    while let Some((current_tree, current_prefix)) = stack.pop() {
        for entry in current_tree.iter() {
            let name = match entry.name() {
                Some(n) => n,
                None => continue,
            };
            let path = current_prefix.join(name);

            match entry.kind() {
                Some(git2::ObjectType::Blob) => {
                    files.push(path);
                }
                Some(git2::ObjectType::Tree) => {
                    if let Ok(obj) = entry.to_object(repo) {
                        if let Ok(subtree) = obj.peel_to_tree() {
                            stack.push((subtree, path));
                        }
                    }
                }
                Some(git2::ObjectType::Commit) => {
                    // 子模块条目:指向一个 commit
                    let submodule_id = entry.id();
                    // 子模块在父仓库工作目录中的实际路径
                    let sm_path = repo.workdir().unwrap_or_else(|| repo.path()).join(&path);
                    match Repository::open(&sm_path) {
                        Ok(sm_repo) => {
                            // 获取子模块期望的提交
                            if let Ok(sm_commit_obj) = sm_repo.find_commit(submodule_id) {
                                if let Ok(sm_tree) = sm_commit_obj.tree() {
                                    // 递归收集子模块文件,前缀保持为相对于父仓库的路径
                                    let sub_files = collect_commit_files(&sm_repo, &sm_tree, &path);
                                    files.extend(sub_files);
                                }
                            }
                        }
                        Err(_) => {
                            // 子模块未初始化或路径不可用,跳过并记录
                            eprintln!(
                                "Warning: submodule '{}' not found or not initialized, skipped.",
                                path.display()
                            );
                        }
                    }
                }
                _ => {}
            }
        }
    }
    files
}