1use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7use tracing::{debug, info};
8use vibe_graph_core::SourceCodeGraph;
9use walkdir::WalkDir;
10
11use crate::error::OpsResult;
12use crate::project::Project;
13use crate::workspace::WorkspaceKind;
14
15pub const SELF_DIR: &str = ".self";
17
18const MANIFEST_FILE: &str = "manifest.json";
19const PROJECT_FILE: &str = "project.json";
20const GRAPH_FILE: &str = "graph.json";
21const SNAPSHOTS_DIR: &str = "snapshots";
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Manifest {
26 pub version: u32,
28
29 pub name: String,
31
32 pub root: PathBuf,
34
35 pub kind: String,
37
38 pub last_sync: SystemTime,
40
41 pub repo_count: usize,
43
44 pub source_count: usize,
46
47 pub total_size: u64,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub remote: Option<String>,
53}
54
55impl Manifest {
56 pub fn from_project(
58 project: &Project,
59 root: &Path,
60 kind: &WorkspaceKind,
61 remote: Option<String>,
62 ) -> Self {
63 Self {
64 version: 1,
65 name: project.name.clone(),
66 root: root.to_path_buf(),
67 kind: kind.to_string(),
68 last_sync: SystemTime::now(),
69 repo_count: project.repositories.len(),
70 source_count: project.total_sources(),
71 total_size: project.total_size(),
72 remote,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct Store {
80 root: PathBuf,
82
83 self_dir: PathBuf,
85}
86
87impl Store {
88 pub fn new(root: impl AsRef<Path>) -> Self {
90 let root = root.as_ref().to_path_buf();
91 let self_dir = root.join(SELF_DIR);
92 Self { root, self_dir }
93 }
94
95 pub fn root(&self) -> &Path {
97 &self.root
98 }
99
100 pub fn self_dir(&self) -> &Path {
102 &self.self_dir
103 }
104
105 pub fn exists(&self) -> bool {
107 self.self_dir.exists()
108 }
109
110 pub fn init(&self) -> OpsResult<()> {
112 if !self.self_dir.exists() {
113 std::fs::create_dir_all(&self.self_dir)?;
114 debug!(path = %self.self_dir.display(), "Created .self directory");
115 }
116
117 let snapshots_dir = self.self_dir.join(SNAPSHOTS_DIR);
118 if !snapshots_dir.exists() {
119 std::fs::create_dir_all(&snapshots_dir)?;
120 }
121
122 Ok(())
123 }
124
125 pub fn save(
127 &self,
128 project: &Project,
129 kind: &WorkspaceKind,
130 remote: Option<String>,
131 ) -> OpsResult<()> {
132 self.init()?;
133
134 let storage_project = strip_content(project);
136
137 let project_path = self.self_dir.join(PROJECT_FILE);
139 let project_json = serde_json::to_string_pretty(&storage_project)?;
140 std::fs::write(&project_path, &project_json)?;
141
142 let manifest = Manifest::from_project(project, &self.root, kind, remote);
144 self.save_manifest(&manifest)?;
145
146 info!(
147 path = %self.self_dir.display(),
148 repos = project.repositories.len(),
149 files = project.total_sources(),
150 "Saved project to .self"
151 );
152
153 Ok(())
154 }
155
156 pub fn save_manifest(&self, manifest: &Manifest) -> OpsResult<()> {
158 self.init()?;
159
160 let manifest_path = self.self_dir.join(MANIFEST_FILE);
161 let manifest_json = serde_json::to_string_pretty(manifest)?;
162 std::fs::write(&manifest_path, &manifest_json)?;
163
164 debug!(path = %manifest_path.display(), "Saved manifest");
165 Ok(())
166 }
167
168 pub fn snapshot(&self, project: &Project) -> OpsResult<PathBuf> {
170 self.init()?;
171
172 let storage_project = strip_content(project);
173 let timestamp = SystemTime::now()
174 .duration_since(SystemTime::UNIX_EPOCH)
175 .unwrap()
176 .as_secs();
177
178 let snapshot_path = self
179 .self_dir
180 .join(SNAPSHOTS_DIR)
181 .join(format!("{}.json", timestamp));
182
183 let json = serde_json::to_string_pretty(&storage_project)?;
184 std::fs::write(&snapshot_path, json)?;
185
186 info!(path = %snapshot_path.display(), "Created snapshot");
187 Ok(snapshot_path)
188 }
189
190 pub fn load(&self) -> OpsResult<Option<Project>> {
192 let project_path = self.self_dir.join(PROJECT_FILE);
193
194 if !project_path.exists() {
195 return Ok(None);
196 }
197
198 let json = std::fs::read_to_string(&project_path)?;
199 let project: Project = serde_json::from_str(&json)?;
200
201 info!(
202 path = %project_path.display(),
203 repos = project.repositories.len(),
204 "Loaded project from .self"
205 );
206
207 Ok(Some(project))
208 }
209
210 pub fn load_manifest(&self) -> OpsResult<Option<Manifest>> {
212 let manifest_path = self.self_dir.join(MANIFEST_FILE);
213
214 if !manifest_path.exists() {
215 return Ok(None);
216 }
217
218 let json = std::fs::read_to_string(&manifest_path)?;
219 let manifest: Manifest = serde_json::from_str(&json)?;
220
221 Ok(Some(manifest))
222 }
223
224 pub fn save_graph(&self, graph: &SourceCodeGraph) -> OpsResult<PathBuf> {
226 self.init()?;
227
228 let graph_path = self.self_dir.join(GRAPH_FILE);
229 let json = serde_json::to_string_pretty(graph)?;
230 std::fs::write(&graph_path, &json)?;
231
232 info!(
233 path = %graph_path.display(),
234 nodes = graph.node_count(),
235 edges = graph.edge_count(),
236 "Saved graph to .self"
237 );
238
239 Ok(graph_path)
240 }
241
242 pub fn load_graph(&self) -> OpsResult<Option<SourceCodeGraph>> {
244 let graph_path = self.self_dir.join(GRAPH_FILE);
245
246 if !graph_path.exists() {
247 return Ok(None);
248 }
249
250 let json = std::fs::read_to_string(&graph_path)?;
251 let graph: SourceCodeGraph = serde_json::from_str(&json)?;
252
253 info!(
254 path = %graph_path.display(),
255 nodes = graph.node_count(),
256 edges = graph.edge_count(),
257 "Loaded graph from .self"
258 );
259
260 Ok(Some(graph))
261 }
262
263 pub fn has_graph(&self) -> bool {
265 self.self_dir.join(GRAPH_FILE).exists()
266 }
267
268 pub fn list_snapshots(&self) -> OpsResult<Vec<PathBuf>> {
270 let snapshots_dir = self.self_dir.join(SNAPSHOTS_DIR);
271
272 if !snapshots_dir.exists() {
273 return Ok(vec![]);
274 }
275
276 let mut snapshots: Vec<PathBuf> = std::fs::read_dir(&snapshots_dir)?
277 .filter_map(|e| e.ok())
278 .map(|e| e.path())
279 .filter(|p| p.extension().map(|e| e == "json").unwrap_or(false))
280 .collect();
281
282 snapshots.sort_by(|a, b| b.cmp(a));
284
285 Ok(snapshots)
286 }
287
288 pub fn load_snapshot(&self, path: &Path) -> OpsResult<Project> {
290 let json = std::fs::read_to_string(path)?;
291 let project: Project = serde_json::from_str(&json)?;
292 Ok(project)
293 }
294
295 pub fn clean(&self) -> OpsResult<()> {
297 if self.self_dir.exists() {
298 std::fs::remove_dir_all(&self.self_dir)?;
299 info!(path = %self.self_dir.display(), "Removed .self directory");
300 }
301 Ok(())
302 }
303
304 pub fn stats(&self) -> OpsResult<StoreStats> {
306 if !self.exists() {
307 return Ok(StoreStats::default());
308 }
309
310 let manifest = self.load_manifest()?;
311 let snapshots = self.list_snapshots()?;
312
313 let total_size = WalkDir::new(&self.self_dir)
315 .into_iter()
316 .filter_map(|e| e.ok())
317 .filter(|e| e.path().is_file())
318 .filter_map(|e| e.metadata().ok())
319 .map(|m| m.len())
320 .sum();
321
322 Ok(StoreStats {
323 exists: true,
324 manifest,
325 snapshot_count: snapshots.len(),
326 total_size,
327 })
328 }
329}
330
331#[derive(Debug, Default, Clone)]
333pub struct StoreStats {
334 pub exists: bool,
336 pub manifest: Option<Manifest>,
338 pub snapshot_count: usize,
340 pub total_size: u64,
342}
343
344fn strip_content(project: &Project) -> Project {
346 let mut stripped = project.clone();
347 for repo in &mut stripped.repositories {
348 for source in &mut repo.sources {
349 source.content = None;
350 }
351 }
352 stripped
353}
354
355pub fn has_store(path: &Path) -> bool {
357 path.join(SELF_DIR).exists()
358}