use serde::{Serialize, Deserialize};
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Result, Context};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DryRunSnapshot {
pub id: String,
pub timestamp: u64,
pub target_path: PathBuf,
pub recipes: Vec<String>,
pub changed_files_count: usize,
pub risky_files_count: usize,
pub warnings: Vec<String>,
}
const DRY_RUN_DIR: &str = ".morph-cli/dry-runs";
pub struct DryRunSnapshotStore {
root: PathBuf,
}
impl DryRunSnapshotStore {
pub fn new(project_root: &Path) -> Self {
Self {
root: project_root.join(DRY_RUN_DIR),
}
}
pub fn save(&self, snapshot: &DryRunSnapshot) -> Result<()> {
fs::create_dir_all(&self.root).with_context(|| {
format!("Failed to create dry-runs snapshots directory: {}", self.root.display())
})?;
let path = self.root.join(format!("{}.json", snapshot.id));
let json = serde_json::to_string_pretty(snapshot)
.context("Failed to serialize dry-run snapshot")?;
fs::write(&path, json)
.with_context(|| format!("Failed to write dry-run snapshot: {}", path.display()))?;
Ok(())
}
pub fn load(&self, id: &str) -> Result<Option<DryRunSnapshot>> {
let path = self.root.join(format!("{}.json", id));
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read dry-run snapshot: {}", path.display()))?;
let snapshot = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse dry-run snapshot: {}", path.display()))?;
Ok(Some(snapshot))
}
pub fn list(&self) -> Result<Vec<DryRunSnapshot>> {
if !self.root.exists() {
return Ok(Vec::new());
}
let mut snapshots = Vec::new();
for entry in fs::read_dir(&self.root)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(snap) = serde_json::from_str::<DryRunSnapshot>(&content) {
snapshots.push(snap);
}
}
}
}
snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
Ok(snapshots)
}
}