paline 0.4.2

一个用 Rust 编写的代码行数统计命令行工具。递归扫描目录,按编程语言统计行数,并以彩色横向条形图在终端中可视化展示。
//! `paline` is a command-line tool written in Rust that counts lines of code.
//!
//! It recursively scans directories, identifies programming languages by file
//! extension / filename, counts lines per language, and visualizes the results
//! as a colorful horizontal bar chart in the terminal.
//!
//! # Usage
//!
//! ```text
//! paline stats                            # Count the current directory
//! paline stats --path /path/to/proj       # Count a specific directory
//! paline stats --commit abc123            # Count the file tree of a specific commit
//! paline diff --base abc123               # Diff a commit against the worktree
//! paline diff --base abc123 --target def456   # Diff between two commits
//! paline stats -vv                        # Enable debug logging
//! ```

use clap::{Parser, Subcommand};
use std::path::Path;
use std::time::Instant;

mod analyzer;
mod color;
mod lang;
mod logger;
mod render;
mod utils;

use crate::logger::{Level, Log, Verbosity, logger};

/// Command-line arguments parsed by clap.
#[derive(Parser, Debug)]
#[command(name = "lcount", version)]
#[command(about = "A command-line tool written in Rust that counts lines of code. It recursively scans directories, counts lines by programming language, and visualizes them in a colorful horizontal bar chart in the terminal.", long_about = None)]
#[command(propagate_version = true)]
struct Args {
    /// Path to the file or directory to scan, defaults to the current directory
    #[arg(short, long, default_value = ".", global = true)]
    path: String,

    /// Logging verbosity; repeat for more detail (`-vv` enables per-file tracing)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    verbose: u8,

    /// Subcommand to run
    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Count lines in a directory or the file tree of a specific git commit
    Stats {
        /// Scan the file tree of a specific commit (requires a git repository)
        #[arg(short, long, value_name = "COMMIT_HASH")]
        commit: Option<String>,
    },

    /// Show the net line change between a base commit and a target commit or the worktree
    Diff {
        /// The base commit to compare against
        #[arg(short, long)]
        base: String,

        /// The target to compare with; a commit hash, or WORKTREE for the working tree
        #[arg(short, long, default_value_t = String::from("WORKTREE"))]
        target: String,
    },
}

/// 程序入口。
///
/// 解析命令行参数,校验路径是否为已存在的目录,然后调用
/// [`analyzer::analyze_project`] 统计文件,再交给 [`render::render`]
/// 输出结果
fn main() {
    let args = Args::parse();
    let path = Path::new(&args.path);

    if !path.exists() || !path.is_dir() {
        logger(
            Log::new(
                Level::Error,
                format!(
                    "The path doesn't exist or isn't a directory: \"{}\". Please check if the path is correct.",
                    &args.path
                ),
                None,
            ),
            None,
        );
        return;
    }

    let verbose = args.verbose;
    let t_start = Instant::now();
    match args.command {
        Commands::Stats { commit } => {
            let stats = if let Some(commit_hash) = &commit {
                // 打开仓库
                let repo = match utils::open_repository(path) {
                    Ok(r) => r,
                    Err(log) => {
                        logger(log, Some(Verbosity::from_u8(&verbose)));
                        return;
                    }
                };

                // 解析提交对象并转为 Commit
                let commit = match utils::get_commit_from(&repo, commit_hash) {
                    Ok(c) => c,
                    Err(log) => {
                        logger(log, Some(Verbosity::from_u8(&verbose)));
                        return;
                    }
                };

                logger(
                    Log::new(
                        Level::Info,
                        format!(
                            "scanning commit {} ({})",
                            &commit_hash[..12.min(commit_hash.len())],
                            commit.summary().unwrap_or("<no message>")
                        ),
                        Some(Verbosity::Verbose),
                    ),
                    Some(Verbosity::from_u8(&verbose)),
                );
                analyzer::analyze_project(
                    &analyzer::Project::Commit {
                        repo: &repo,
                        commit: &commit,
                    },
                    &verbose,
                )
            } else {
                analyzer::analyze_project(&analyzer::Project::Disk { root: path }, &verbose)
            };

            // 打印分析时间
            let t_analyzed = t_start.elapsed();
            logger(
                Log::new(
                    Level::Info,
                    format!("analyze: {:.2?}", t_analyzed),
                    Some(Verbosity::Verbose),
                ),
                Some(Verbosity::from_u8(&verbose)),
            );

            // 返回 render 的输出
            if let Err(log) = render::render(render::Mode::Stats { stats }) {
                logger(log, Some(Verbosity::from_u8(&verbose)));
            }
        }

        Commands::Diff { base, target } => {
            // 打开仓库
            let repo = match utils::open_repository(path) {
                Ok(r) => r,
                Err(log) => {
                    logger(log, Some(Verbosity::from_u8(&verbose)));
                    return;
                }
            };

            // 解析 base commit
            let base_commit = match utils::get_commit_from(&repo, &base) {
                Ok(c) => c,
                Err(log) => {
                    logger(log, Some(Verbosity::from_u8(&verbose)));
                    return;
                }
            };

            logger(
                Log::new(
                    Level::Info,
                    format!(
                        "diff base commit {} ({})",
                        &base[..12.min(base.len())],
                        base_commit.summary().unwrap_or("<no message>")
                    ),
                    Some(Verbosity::Verbose),
                ),
                Some(Verbosity::from_u8(&verbose)),
            );

            let base_project = analyzer::Project::Commit {
                repo: &repo,
                commit: &base_commit,
            };
            let base_stats = analyzer::analyze_project(&base_project, &verbose);

            // target 可以是 WORKTREE(默认)或另一个 commit hash
            let target_stats = if target.eq_ignore_ascii_case("WORKTREE") {
                logger(
                    Log::new(
                        Level::Info,
                        "target: WORKTREE".to_string(),
                        Some(Verbosity::Verbose),
                    ),
                    Some(Verbosity::from_u8(&verbose)),
                );
                analyzer::analyze_project(&analyzer::Project::Disk { root: path }, &verbose)
            } else {
                let target_commit = match utils::get_commit_from(&repo, &target) {
                    Ok(c) => c,
                    Err(log) => {
                        logger(log, Some(Verbosity::from_u8(&verbose)));
                        return;
                    }
                };
                logger(
                    Log::new(
                        Level::Info,
                        format!(
                            "target commit {} ({})",
                            &target[..12.min(target.len())],
                            target_commit.summary().unwrap_or("<no message>")
                        ),
                        Some(Verbosity::Verbose),
                    ),
                    Some(Verbosity::from_u8(&verbose)),
                );
                analyzer::analyze_project(
                    &analyzer::Project::Commit {
                        repo: &repo,
                        commit: &target_commit,
                    },
                    &verbose,
                )
            };

            // 打印分析时间
            let t_analyzed = t_start.elapsed();
            logger(
                Log::new(
                    Level::Info,
                    format!("analyze: {:.2?}", t_analyzed),
                    Some(Verbosity::Verbose),
                ),
                Some(Verbosity::from_u8(&verbose)),
            );

            // 返回 render 的输出
            if let Err(log) = render::render(render::Mode::Diff {
                base: base_stats,
                target: target_stats,
            }) {
                logger(log, Some(Verbosity::from_u8(&verbose)));
            }
        }
    }
}