1use std::fs;
2use std::path::{Path, PathBuf};
3
4use directories::ProjectDirs;
5use serde::{Deserialize, Serialize};
6
7use crate::actions::ActionCatalog;
8use crate::error::{GitLgError, Result};
9use crate::models::GraphQuery;
10
11const DEFAULT_STATE_FILENAME: &str = "state.json";
12const CURRENT_SCHEMA_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct AppState {
16 pub schema_version: u32,
17 pub selected_repo_path: Option<PathBuf>,
18 pub preferred_git_binary: Option<String>,
19 pub default_remote_name: String,
20 pub graph_query: GraphQuery,
21 pub selected_commit_hashes: Vec<String>,
22 pub actions: ActionCatalog,
23}
24
25impl Default for AppState {
26 fn default() -> Self {
27 Self {
28 schema_version: CURRENT_SCHEMA_VERSION,
29 selected_repo_path: None,
30 preferred_git_binary: None,
31 default_remote_name: "origin".to_string(),
32 graph_query: GraphQuery::default(),
33 selected_commit_hashes: Vec::new(),
34 actions: ActionCatalog::with_defaults(),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40pub struct StateStore {
41 path: PathBuf,
42}
43
44impl StateStore {
45 pub fn at(path: PathBuf) -> Self {
46 Self { path }
47 }
48
49 pub fn default_location() -> Result<PathBuf> {
50 let project_dirs = ProjectDirs::from("dev", "GitGraph", "gitgraph")
51 .ok_or_else(|| GitLgError::State("cannot resolve project directories".to_string()))?;
52 Ok(project_dirs.config_dir().join(DEFAULT_STATE_FILENAME))
53 }
54
55 pub fn default_store() -> Result<Self> {
56 Ok(Self {
57 path: Self::default_location()?,
58 })
59 }
60
61 pub fn path(&self) -> &Path {
62 &self.path
63 }
64
65 pub fn load(&self) -> Result<AppState> {
66 if !self.path.exists() {
67 return Ok(AppState::default());
68 }
69 let text = fs::read_to_string(&self.path)
70 .map_err(|source| GitLgError::io("reading state file", source))?;
71 let mut state: AppState = serde_json::from_str(&text)
72 .map_err(|e| GitLgError::State(format!("invalid state json: {}", e)))?;
73 if state.schema_version == 0 {
74 state.schema_version = CURRENT_SCHEMA_VERSION;
75 }
76 Ok(state)
77 }
78
79 pub fn save(&self, state: &AppState) -> Result<()> {
80 if let Some(parent) = self.path.parent() {
81 fs::create_dir_all(parent)
82 .map_err(|source| GitLgError::io("creating state directory", source))?;
83 }
84 let text = serde_json::to_string_pretty(state)
85 .map_err(|e| GitLgError::State(format!("serialize state failed: {}", e)))?;
86 fs::write(&self.path, text).map_err(|source| GitLgError::io("writing state file", source))
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use tempfile::TempDir;
93
94 use super::{AppState, StateStore};
95
96 #[test]
97 fn roundtrip_state_file() {
98 let tmp = TempDir::new().expect("tempdir");
99 let store = StateStore::at(tmp.path().join("state.json"));
100
101 let mut state = AppState::default();
102 state.selected_repo_path = Some(tmp.path().to_path_buf());
103 state.default_remote_name = "upstream".to_string();
104
105 store.save(&state).expect("save state");
106 let loaded = store.load().expect("load state");
107 assert_eq!(loaded.default_remote_name, "upstream");
108 assert_eq!(loaded.selected_repo_path, state.selected_repo_path);
109 }
110}