vkit 0.1.4

Fast Rust dev CLI: manage git worktrees, Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! Placement:约定落盘位置的分类与路径计算。

use std::path::{Path, PathBuf};

/// Worktree 落盘位置类别(不含 Main;Main 单独处理)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
    InRepo,
    Global,
}

/// 仓库内 In-repo 根:`<main>/.worktrees`。
pub fn in_repo_root(main_worktree: &Path) -> PathBuf {
    main_worktree.join(".worktrees")
}

/// Global 下本仓库根:`<global_root>/<repo_name>`。
pub fn global_repo_root(global_root: &Path, repo_name: &str) -> PathBuf {
    global_root.join(repo_name)
}

/// 给定 Placement 与 Worktree Name,算出目标目录。
pub fn worktree_path(
    placement: Placement,
    main_worktree: &Path,
    global_root: &Path,
    repo_name: &str,
    name: &str,
) -> PathBuf {
    match placement {
        Placement::InRepo => in_repo_root(main_worktree).join(name),
        Placement::Global => global_repo_root(global_root, repo_name).join(name),
    }
}

/// 判断 `path` 属于哪种约定 Placement;都不属于则 `None`(不管理)。
pub fn classify(
    path: &Path,
    main_worktree: &Path,
    global_root: &Path,
    repo_name: &str,
) -> Option<Placement> {
    let path = normalize(path);
    let in_repo = normalize(&in_repo_root(main_worktree));
    if path.starts_with(&in_repo) {
        return Some(Placement::InRepo);
    }
    let global = normalize(&global_repo_root(global_root, repo_name));
    if path.starts_with(&global) {
        return Some(Placement::Global);
    }
    None
}

fn normalize(path: &Path) -> PathBuf {
    // 测试与展示用:去掉末尾 `/`,不解析 symlink(避免依赖真实 FS)。
    let mut s = path.to_string_lossy().to_string();
    while s.ends_with('/') && s.len() > 1 {
        s.pop();
    }
    PathBuf::from(s)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn main() -> PathBuf {
        PathBuf::from("/proj/vkit-rs")
    }

    fn global() -> PathBuf {
        PathBuf::from("/Users/me/worktrees")
    }

    #[test]
    fn classifies_in_repo() {
        let p = PathBuf::from("/proj/vkit-rs/.worktrees/feat-auth");
        assert_eq!(
            classify(&p, &main(), &global(), "vkit-rs"),
            Some(Placement::InRepo)
        );
    }

    #[test]
    fn classifies_global() {
        let p = PathBuf::from("/Users/me/worktrees/vkit-rs/feat-auth");
        assert_eq!(
            classify(&p, &main(), &global(), "vkit-rs"),
            Some(Placement::Global)
        );
    }

    #[test]
    fn rejects_other_paths() {
        let p = PathBuf::from("/tmp/odd-worktree");
        assert_eq!(classify(&p, &main(), &global(), "vkit-rs"), None);
    }

    #[test]
    fn rejects_other_repo_under_global() {
        let p = PathBuf::from("/Users/me/worktrees/other-repo/feat-auth");
        assert_eq!(classify(&p, &main(), &global(), "vkit-rs"), None);
    }

    #[test]
    fn builds_paths() {
        assert_eq!(
            worktree_path(Placement::InRepo, &main(), &global(), "vkit-rs", "feat-auth"),
            PathBuf::from("/proj/vkit-rs/.worktrees/feat-auth")
        );
        assert_eq!(
            worktree_path(Placement::Global, &main(), &global(), "vkit-rs", "feat-auth"),
            PathBuf::from("/Users/me/worktrees/vkit-rs/feat-auth")
        );
    }
}