morph_cli/core/
dry_run.rs1use serde::{Serialize, Deserialize};
2use std::fs;
3use std::path::{Path, PathBuf};
4use anyhow::{Result, Context};
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct DryRunSnapshot {
8 pub id: String,
9 pub timestamp: u64,
10 pub target_path: PathBuf,
11 pub recipes: Vec<String>,
12 pub changed_files_count: usize,
13 pub risky_files_count: usize,
14 pub warnings: Vec<String>,
15}
16
17const DRY_RUN_DIR: &str = ".morph-cli/dry-runs";
18
19pub struct DryRunSnapshotStore {
20 root: PathBuf,
21}
22
23impl DryRunSnapshotStore {
24 pub fn new(project_root: &Path) -> Self {
25 Self {
26 root: project_root.join(DRY_RUN_DIR),
27 }
28 }
29
30 pub fn save(&self, snapshot: &DryRunSnapshot) -> Result<()> {
31 fs::create_dir_all(&self.root).with_context(|| {
32 format!("Failed to create dry-runs snapshots directory: {}", self.root.display())
33 })?;
34
35 let path = self.root.join(format!("{}.json", snapshot.id));
36 let json = serde_json::to_string_pretty(snapshot)
37 .context("Failed to serialize dry-run snapshot")?;
38 fs::write(&path, json)
39 .with_context(|| format!("Failed to write dry-run snapshot: {}", path.display()))?;
40 Ok(())
41 }
42
43 pub fn load(&self, id: &str) -> Result<Option<DryRunSnapshot>> {
44 let path = self.root.join(format!("{}.json", id));
45 if !path.exists() {
46 return Ok(None);
47 }
48
49 let content = fs::read_to_string(&path)
50 .with_context(|| format!("Failed to read dry-run snapshot: {}", path.display()))?;
51 let snapshot = serde_json::from_str(&content)
52 .with_context(|| format!("Failed to parse dry-run snapshot: {}", path.display()))?;
53 Ok(Some(snapshot))
54 }
55
56 pub fn list(&self) -> Result<Vec<DryRunSnapshot>> {
57 if !self.root.exists() {
58 return Ok(Vec::new());
59 }
60
61 let mut snapshots = Vec::new();
62 for entry in fs::read_dir(&self.root)? {
63 let entry = entry?;
64 let path = entry.path();
65 if path.extension().and_then(|s| s.to_str()) == Some("json") {
66 if let Ok(content) = fs::read_to_string(&path) {
67 if let Ok(snap) = serde_json::from_str::<DryRunSnapshot>(&content) {
68 snapshots.push(snap);
69 }
70 }
71 }
72 }
73
74 snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
75 Ok(snapshots)
76 }
77}