gitsw 0.1.0

A smart Git branch switcher with automatic stash management and dependency installation
Documentation
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

const STATE_FILE_NAME: &str = "git-switch.json";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchState {
    /// The OID of the stash created when leaving this branch
    pub stash_id: Option<String>,
    /// SHA256 hash of the lock file when we last visited this branch
    pub lock_file_hash: Option<String>,
    /// Timestamp of last visit to this branch
    pub last_visited: DateTime<Utc>,
}

impl Default for BranchState {
    fn default() -> Self {
        Self {
            stash_id: None,
            lock_file_hash: None,
            last_visited: Utc::now(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct StateData {
    pub branches: HashMap<String, BranchState>,
}

pub struct StateManager {
    path: PathBuf,
    data: StateData,
}

impl StateManager {
    /// Load state from .git/git-switch.json
    pub fn load(git_dir: &Path) -> Result<Self> {
        let path = git_dir.join(STATE_FILE_NAME);

        let data = if path.exists() {
            let content = fs::read_to_string(&path)
                .with_context(|| format!("Failed to read state file: {}", path.display()))?;
            serde_json::from_str(&content).unwrap_or_default()
        } else {
            StateData::default()
        };

        Ok(Self { path, data })
    }

    /// Save state to .git/git-switch.json
    pub fn save(&self) -> Result<()> {
        let content = serde_json::to_string_pretty(&self.data)?;
        fs::write(&self.path, content)
            .with_context(|| format!("Failed to write state file: {}", self.path.display()))?;
        Ok(())
    }

    /// Get state for a specific branch
    pub fn get_branch(&self, branch_name: &str) -> Option<&BranchState> {
        self.data.branches.get(branch_name)
    }

    /// Set state for a branch
    pub fn set_branch(&mut self, branch_name: &str, state: BranchState) {
        self.data.branches.insert(branch_name.to_string(), state);
    }

    /// Update the stash ID for a branch
    pub fn set_stash(&mut self, branch_name: &str, stash_id: Option<String>) {
        let state = self
            .data
            .branches
            .entry(branch_name.to_string())
            .or_default();
        state.stash_id = stash_id;
        state.last_visited = Utc::now();
    }

    /// Update the lock file hash for a branch
    pub fn set_lock_hash(&mut self, branch_name: &str, hash: Option<String>) {
        let state = self
            .data
            .branches
            .entry(branch_name.to_string())
            .or_default();
        state.lock_file_hash = hash;
        state.last_visited = Utc::now();
    }

    /// Get all branches with stashes
    pub fn branches_with_stashes(&self) -> Vec<(&str, &BranchState)> {
        self.data
            .branches
            .iter()
            .filter(|(_, state)| state.stash_id.is_some())
            .map(|(name, state)| (name.as_str(), state))
            .collect()
    }

    /// Remove branch state
    pub fn remove_branch(&mut self, branch_name: &str) {
        self.data.branches.remove(branch_name);
    }

    /// Clear stash from branch state
    pub fn clear_stash(&mut self, branch_name: &str) {
        if let Some(state) = self.data.branches.get_mut(branch_name) {
            state.stash_id = None;
        }
    }

    /// Get recent branches sorted by last visited time
    pub fn recent_branches(&self, limit: usize) -> Vec<(&str, &BranchState)> {
        let mut branches: Vec<_> = self
            .data
            .branches
            .iter()
            .map(|(name, state)| (name.as_str(), state))
            .collect();

        branches.sort_by(|a, b| b.1.last_visited.cmp(&a.1.last_visited));
        branches.truncate(limit);
        branches
    }

    /// Update last visited timestamp for a branch
    pub fn touch_branch(&mut self, branch_name: &str) {
        let state = self
            .data
            .branches
            .entry(branch_name.to_string())
            .or_default();
        state.last_visited = Utc::now();
    }
}