gitsw 0.1.0

A smart Git branch switcher with automatic stash management and dependency installation
Documentation
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use std::process::{Command, Stdio};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PackageManager {
    Npm,
    Yarn,
    Pnpm,
}

impl PackageManager {
    pub fn lock_file(&self) -> &'static str {
        match self {
            PackageManager::Npm => "package-lock.json",
            PackageManager::Yarn => "yarn.lock",
            PackageManager::Pnpm => "pnpm-lock.yaml",
        }
    }

    pub fn install_command(&self) -> &'static str {
        match self {
            PackageManager::Npm => "npm",
            PackageManager::Yarn => "yarn",
            PackageManager::Pnpm => "pnpm",
        }
    }

    pub fn install_args(&self) -> &'static [&'static str] {
        match self {
            PackageManager::Npm => &["install"],
            PackageManager::Yarn => &["install"],
            PackageManager::Pnpm => &["install"],
        }
    }

    pub fn name(&self) -> &'static str {
        match self {
            PackageManager::Npm => "npm",
            PackageManager::Yarn => "yarn",
            PackageManager::Pnpm => "pnpm",
        }
    }
}

/// Detect which package manager is being used by checking for lock files
pub fn detect_package_manager(workdir: &Path) -> Option<PackageManager> {
    // Check in order of preference
    let managers = [
        PackageManager::Pnpm,
        PackageManager::Yarn,
        PackageManager::Npm,
    ];

    managers
        .into_iter()
        .find(|pm| workdir.join(pm.lock_file()).exists())
}

/// Calculate SHA256 hash of a file
pub fn get_file_hash(path: &Path) -> Result<String> {
    let content =
        fs::read(path).with_context(|| format!("Failed to read file: {}", path.display()))?;
    let mut hasher = Sha256::new();
    hasher.update(&content);
    let result = hasher.finalize();
    Ok(format!("{:x}", result))
}

/// Get the hash of the lock file for the detected package manager
pub fn get_lock_file_hash(workdir: &Path) -> Result<Option<(PackageManager, String)>> {
    if let Some(pm) = detect_package_manager(workdir) {
        let lock_path = workdir.join(pm.lock_file());
        let hash = get_file_hash(&lock_path)?;
        Ok(Some((pm, hash)))
    } else {
        Ok(None)
    }
}

/// Run the install command for the given package manager
pub fn run_install(pm: PackageManager, workdir: &Path) -> Result<bool> {
    let mut child = Command::new(pm.install_command())
        .args(pm.install_args())
        .current_dir(workdir)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()
        .with_context(|| format!("Failed to run {} install", pm.name()))?;

    let status = child.wait()?;
    Ok(status.success())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::TempDir;

    #[test]
    fn test_detect_npm() {
        let dir = TempDir::new().unwrap();
        fs::write(dir.path().join("package-lock.json"), "{}").unwrap();
        assert_eq!(
            detect_package_manager(dir.path()),
            Some(PackageManager::Npm)
        );
    }

    #[test]
    fn test_detect_yarn() {
        let dir = TempDir::new().unwrap();
        fs::write(dir.path().join("yarn.lock"), "").unwrap();
        assert_eq!(
            detect_package_manager(dir.path()),
            Some(PackageManager::Yarn)
        );
    }

    #[test]
    fn test_detect_pnpm() {
        let dir = TempDir::new().unwrap();
        fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
        assert_eq!(
            detect_package_manager(dir.path()),
            Some(PackageManager::Pnpm)
        );
    }

    #[test]
    fn test_no_package_manager() {
        let dir = TempDir::new().unwrap();
        assert_eq!(detect_package_manager(dir.path()), None);
    }

    #[test]
    fn test_file_hash() {
        let dir = TempDir::new().unwrap();
        let file = dir.path().join("test.txt");
        let mut f = fs::File::create(&file).unwrap();
        f.write_all(b"hello world").unwrap();

        let hash = get_file_hash(&file).unwrap();
        // SHA256 of "hello world"
        assert_eq!(
            hash,
            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
        );
    }
}