Skip to main content

morph_cli/core/
dry_run.rs

1use 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}