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 {
pub stash_id: Option<String>,
pub lock_file_hash: Option<String>,
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 {
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 })
}
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(())
}
pub fn get_branch(&self, branch_name: &str) -> Option<&BranchState> {
self.data.branches.get(branch_name)
}
pub fn set_branch(&mut self, branch_name: &str, state: BranchState) {
self.data.branches.insert(branch_name.to_string(), state);
}
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();
}
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();
}
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()
}
pub fn remove_branch(&mut self, branch_name: &str) {
self.data.branches.remove(branch_name);
}
pub fn clear_stash(&mut self, branch_name: &str) {
if let Some(state) = self.data.branches.get_mut(branch_name) {
state.stash_id = None;
}
}
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
}
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();
}
}