ls-plus 0.0.1

Enhanced ls command with modern features - supports both cargo and npm installation
// 导入依赖项
use crate::error::{LsPlusError, Result}; // 错误处理类型
use std::path::Path; // 路径类型
use std::process::Command; // 系统命令执行

/// Git 仓库状态信息结构体
///
/// 包含了 Git 仓库的基本信息和文件状态列表。
/// 用于在文件列表中显示 Git 相关的状态信息。
#[derive(Debug, Clone)]
pub struct GitStatus {
    /// 当前目录是否为 Git 仓库
    pub is_repo: bool,
    /// 当前分支名称(如果有的话)
    pub branch: Option<String>,
    /// 仓库中各个文件的状态列表
    pub files: Vec<GitFileStatus>,
}

/// Git 文件状态信息结构体
///
/// 表示仓库中单个文件的 Git 状态信息。
#[derive(Debug, Clone)]
pub struct GitFileStatus {
    /// 文件的相对路径
    pub path: String,
    /// 文件的 Git 状态
    pub status: GitFileState,
}

/// Git 文件状态枚举
///
/// 定义了文件在 Git 仓库中可能的各种状态。
/// 这些状态对应于 `git status --porcelain` 命令的输出。
#[derive(Debug, Clone, PartialEq)]
pub enum GitFileState {
    /// 未跟踪的文件(新文件,尚未添加到仓库)
    Untracked,
    /// 已修改的文件(工作区或暂存区有变更)
    Modified,
    /// 新添加到暂存区的文件
    Added,
    /// 已删除的文件
    Deleted,
    /// 重命名的文件
    Renamed,
    /// 复制的文件
    Copied,
    /// 被忽略的文件(在 .gitignore 中)
    Ignored,
    /// 无变更的文件(与 HEAD 一致)
    Clean,
}

impl Default for GitStatus {
    fn default() -> Self {
        Self::new()
    }
}

impl GitStatus {
    /// 创建一个空的 GitStatus 实例
    ///
    /// 用于表示非 Git 仓库的状态。
    ///
    /// # 返回值
    /// * `Self` - 新的 GitStatus 实例,所有字段都设为默认值
    pub fn new() -> Self {
        Self {
            is_repo: false,    // 不是 Git 仓库
            branch: None,      // 没有分支信息
            files: Vec::new(), // 空的文件状态列表
        }
    }

    /// 获取指定文件的 Git 状态
    ///
    /// 在文件状态列表中查找指定路径的文件,返回其 Git 状态。
    /// 如果文件不在列表中,则认为其状态为 Clean。
    ///
    /// # 参数
    /// * `file_path` - 文件的相对路径
    ///
    /// # 返回值
    /// * `GitFileState` - 文件的 Git 状态
    pub fn get_file_status(&self, file_path: &str) -> GitFileState {
        self.files
            .iter()
            .find(|f| f.path == file_path) // 查找匹配的文件
            .map(|f| f.status.clone()) // 获取其状态
            .unwrap_or(GitFileState::Clean) // 默认为 Clean 状态
    }
}

/// 获取指定路径的 Git 状态信息
///
/// 检查指定路径是否为 Git 仓库,如果是则获取当前分支和文件状态信息。
/// 这是 Git 集成功能的主入口点。
///
/// # 参数
/// * `path` - 要检查的路径
///
/// # 返回值
/// * `Result<GitStatus>` - 成功时返回 Git 状态信息,失败时返回错误
///
/// # 错误
/// * Git 命令执行失败
/// * 读取 Git 状态输出失败
/// * 路径不是有效的 Git 仓库
///
/// # 功能
/// - 检查路径是否为 Git 仓库
/// - 获取当前分支名称
/// - 解析 `git status --porcelain` 输出
/// - 返回结构化的状态信息
pub async fn get_git_status(path: &Path) -> Result<GitStatus> {
    if !is_git_repo(path).await? {
        return Ok(GitStatus::new());
    }

    let mut git_status = GitStatus {
        is_repo: true,
        branch: get_current_branch(path).await?,
        files: Vec::new(),
    };

    // 获取文件的 Git 状态信息
    // 执行 `git status --porcelain` 命令来获取机器可读的状态输出
    let output = Command::new("git")
        .args(["status", "--porcelain"]) // --porcelain 提供简洁的机器可读格式
        .current_dir(path) // 在指定目录中执行命令
        .output()
        .map_err(|e| LsPlusError::git_error(format!("Failed to run git status: {e}")))?;

    // 检查命令是否成功执行
    if !output.status.success() {
        return Err(LsPlusError::git_error("Git status command failed"));
    }

    // 将命令输出转换为字符串(处理可能的非 UTF-8 字符)
    let status_text = String::from_utf8_lossy(&output.stdout);

    // 解析每一行的状态信息
    for line in status_text.lines() {
        // 跳过长度不足的行(有效的状态行至少3个字符)
        if line.len() < 3 {
            continue;
        }

        // Git porcelain 格式:前两个字符是状态码,第3个字符是空格,后面是文件路径
        let status_code = &line[0..2]; // 提取状态码
        let file_path = &line[3..]; // 提取文件路径

        // 根据状态码确定文件状态
        // Git status 状态码说明:
        // ?? = 未跟踪文件
        // A  = 新文件已添加到暂存区
        //  A = 新文件已添加到暂存区(另一种表示)
        // M  = 文件已修改并在暂存区
        //  M = 文件已修改但未暂存
        // MM = 文件在暂存区和工作区都有修改
        // D  = 文件已删除并在暂存区
        //  D = 文件已删除但未暂存
        // R  = 文件已重命名
        // C  = 文件已复制
        // !! = 文件被忽略
        let status = match status_code {
            "??" => GitFileState::Untracked,              // 未跟踪文件
            "A " | " A" => GitFileState::Added,           // 新添加的文件
            "M " | " M" | "MM" => GitFileState::Modified, // 已修改的文件
            "D " | " D" => GitFileState::Deleted,         // 已删除的文件
            "R " | " R" => GitFileState::Renamed,         // 重命名的文件
            "C " | " C" => GitFileState::Copied,          // 复制的文件
            "!!" => GitFileState::Ignored,                // 被忽略的文件
            _ => GitFileState::Modified,                  // 其他状态默认为已修改
        };

        // 将文件状态信息添加到状态列表中
        git_status.files.push(GitFileStatus {
            path: file_path.to_string(),
            status,
        });
    }

    Ok(git_status)
}

/// 检查指定路径是否为 Git 仓库
///
/// 使用 `git rev-parse --is-inside-work-tree` 命令来检查当前目录是否
/// 位于 Git 工作树内部。这是检测 Git 仓库的标准方法。
///
/// # 参数
/// * `path` - 要检查的目录路径
///
/// # 返回值
/// * `Result<bool>` - 如果是 Git 仓库返回 true,否则返回 false
///
/// # 错误
/// * Git 命令执行失败
///
/// # 工作原理
/// `git rev-parse --is-inside-work-tree` 命令:
/// - 如果在 Git 工作树内,返回 "true" 并退出码为 0
/// - 如果不在 Git 工作树内,退出码非 0
async fn is_git_repo(path: &Path) -> Result<bool> {
    let output = Command::new("git")
        .args(["rev-parse", "--is-inside-work-tree"]) // 检查是否在工作树内
        .current_dir(path) // 在指定目录执行
        .output()
        .map_err(|_| LsPlusError::git_error("Failed to check if directory is a git repository"))?;

    // 如果命令成功执行(退出码为0),说明是 Git 仓库
    Ok(output.status.success())
}

/// 获取当前 Git 分支名称
///
/// 使用 `git branch --show-current` 命令获取当前检出的分支名称。
/// 如果当前不在任何分支上(例如在 detached HEAD 状态),则返回 None。
///
/// # 参数
/// * `path` - Git 仓库的路径
///
/// # 返回值
/// * `Result<Option<String>>` - 成功时返回分支名称,如果无分支则为 None
///
/// # 错误
/// * Git 命令执行失败
///
/// # 特殊情况
/// - 如果在 detached HEAD 状态,命令会成功但输出为空
/// - 如果在新仓库中没有任何提交,可能也会返回空输出
async fn get_current_branch(path: &Path) -> Result<Option<String>> {
    let output = Command::new("git")
        .args(["branch", "--show-current"]) // 显示当前分支名称
        .current_dir(path) // 在指定目录执行
        .output()
        .map_err(|e| LsPlusError::git_error(format!("Failed to get current branch: {e}")))?;

    // 如果命令执行失败,返回 None(可能是没有分支或其他错误)
    if !output.status.success() {
        return Ok(None);
    }

    // 将输出转换为字符串并去除首尾空白字符
    let branch = String::from_utf8_lossy(&output.stdout)
        .trim() // 去除换行符和空格
        .to_string();

    // 如果分支名为空(例如 detached HEAD 状态),返回 None
    if branch.is_empty() {
        Ok(None)
    } else {
        Ok(Some(branch))
    }
}

/// 获取 Git 文件状态对应的符号表示
///
/// 将文件的 Git 状态映射为对应的符号字符串,这些符号与 `git status --porcelain`
/// 输出的状态码保持一致,便于用户理解。
///
/// # 参数
/// * `status` - 文件的 Git 状态
///
/// # 返回值
/// * `&'static str` - 对应的状态符号字符串
///
/// # 符号说明
/// - `??`: 未跟踪文件(新文件,未添加到版本控制)
/// - `M `: 已修改文件(有变更内容)
/// - `A `: 新添加文件(已添加到暂存区)
/// - `D `: 已删除文件(从版本控制中删除)
/// - `R `: 重命名文件(文件被重命名)
/// - `C `: 复制文件(文件被复制)
/// - `!!`: 被忽略文件(在 .gitignore 中指定)
/// - `  `: 无变更文件(与 HEAD 提交一致)
pub fn get_git_status_symbol(status: &GitFileState) -> &'static str {
    match status {
        GitFileState::Untracked => "??", // 未跟踪文件
        GitFileState::Modified => "M ",  // 已修改文件
        GitFileState::Added => "A ",     // 新添加文件
        GitFileState::Deleted => "D ",   // 已删除文件
        GitFileState::Renamed => "R ",   // 重命名文件
        GitFileState::Copied => "C ",    // 复制文件
        GitFileState::Ignored => "!!",   // 被忽略文件
        GitFileState::Clean => "  ",     // 无变更文件(两个空格)
    }
}

/// 获取 Git 文件状态对应的显示颜色
///
/// 为不同的 Git 文件状态分配合适的颜色,以提供直观的视觉反馈。
/// 颜色选择遵循常见的 Git 工具和终端的颜色约定。
///
/// # 参数
/// * `status` - 文件的 Git 状态
///
/// # 返回值
/// * `console::Color` - 对应的终端显示颜色
///
/// # 颜色说明
/// - 红色:表示需要注意的状态(未跟踪、已删除)
/// - 黄色:表示有变更但未最终确定(已修改)
/// - 绿色:表示积极的状态(新添加到版本控制)
/// - 蓝色:表示文件操作(重命名、复制)
/// - 黑色:表示不重要的文件(被忽略)
/// - 白色:表示正常状态(无变更)
pub fn get_git_status_color(status: &GitFileState) -> console::Color {
    match status {
        GitFileState::Untracked => console::Color::Red, // 红色 - 未跟踪文件(需要注意)
        GitFileState::Modified => console::Color::Yellow, // 黄色 - 已修改文件(有变更)
        GitFileState::Added => console::Color::Green,   // 绿色 - 新添加文件(积极状态)
        GitFileState::Deleted => console::Color::Red,   // 红色 - 已删除文件(需要注意)
        GitFileState::Renamed => console::Color::Blue,  // 蓝色 - 重命名文件(文件操作)
        GitFileState::Copied => console::Color::Blue,   // 蓝色 - 复制文件(文件操作)
        GitFileState::Ignored => console::Color::Black, // 黑色 - 被忽略文件(不重要)
        GitFileState::Clean => console::Color::White,   // 白色 - 无变更文件(正常状态)
    }
}