chub_core/team/
snapshots.rs1use std::fs;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{Error, Result};
7use crate::team::pins::{load_pins, save_pins, PinEntry, PinsFile};
8use crate::team::project::project_chub_dir;
9
10fn validate_name(name: &str) -> Result<()> {
11 crate::util::validate_filename(name, "snapshot")
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Snapshot {
17 pub name: String,
18 pub created_at: String,
19 pub pins: Vec<PinEntry>,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct SnapshotDiff {
25 pub id: String,
26 pub change: DiffChange,
27}
28
29#[derive(Debug, Clone, Serialize)]
30#[serde(rename_all = "lowercase")]
31pub enum DiffChange {
32 Added {
33 version: Option<String>,
34 },
35 Removed {
36 version: Option<String>,
37 },
38 Changed {
39 from_version: Option<String>,
40 to_version: Option<String>,
41 },
42}
43
44fn snapshots_dir() -> Option<PathBuf> {
45 project_chub_dir().map(|d| d.join("snapshots"))
46}
47
48fn now_iso() -> String {
49 crate::util::now_iso8601()
50}
51
52pub fn create_snapshot(name: &str) -> Result<Snapshot> {
54 validate_name(name)?;
55 let dir =
56 snapshots_dir().ok_or_else(|| Error::Config("No .chub/ directory found.".to_string()))?;
57 fs::create_dir_all(&dir)?;
58
59 let pins = load_pins();
60 let snapshot = Snapshot {
61 name: name.to_string(),
62 created_at: now_iso(),
63 pins: pins.pins,
64 };
65
66 let path = dir.join(format!("{}.yaml", name));
67 let yaml = serde_yaml::to_string(&snapshot).map_err(|e| Error::Config(e.to_string()))?;
68 fs::write(&path, yaml)?;
69
70 Ok(snapshot)
71}
72
73pub fn restore_snapshot(name: &str) -> Result<Snapshot> {
76 validate_name(name)?;
77 let dir =
78 snapshots_dir().ok_or_else(|| Error::Config("No .chub/ directory found.".to_string()))?;
79
80 let path = dir.join(format!("{}.yaml", name));
81 if !path.exists() {
82 return Err(Error::Config(format!("Snapshot \"{}\" not found.", name)));
83 }
84
85 let raw = fs::read_to_string(&path)?;
86 let snapshot: Snapshot =
87 serde_yaml::from_str(&raw).map_err(|e| Error::Config(e.to_string()))?;
88
89 let current_pins = load_pins();
91 if !current_pins.pins.is_empty() {
92 let backup_name = format!("pre-restore-{}", name);
93 let backup_path = dir.join(format!("{}.yaml", backup_name));
94 if !backup_path.exists() {
95 let backup = Snapshot {
96 name: backup_name,
97 created_at: now_iso(),
98 pins: current_pins.pins,
99 };
100 if let Ok(yaml) = serde_yaml::to_string(&backup) {
101 let _ = fs::write(&backup_path, yaml);
102 }
103 }
104 }
105
106 let pins_file = PinsFile {
107 pins: snapshot.pins.clone(),
108 };
109 save_pins(&pins_file)?;
110
111 Ok(snapshot)
112}
113
114pub fn diff_snapshots(name_a: &str, name_b: &str) -> Result<Vec<SnapshotDiff>> {
116 validate_name(name_a)?;
117 validate_name(name_b)?;
118 let dir =
119 snapshots_dir().ok_or_else(|| Error::Config("No .chub/ directory found.".to_string()))?;
120
121 let load = |name: &str| -> Result<Snapshot> {
122 let path = dir.join(format!("{}.yaml", name));
123 if !path.exists() {
124 return Err(Error::Config(format!("Snapshot \"{}\" not found.", name)));
125 }
126 let raw = fs::read_to_string(&path)?;
127 serde_yaml::from_str(&raw).map_err(|e| Error::Config(e.to_string()))
128 };
129
130 let snap_a = load(name_a)?;
131 let snap_b = load(name_b)?;
132
133 let mut diffs = Vec::new();
134
135 for pin_b in &snap_b.pins {
137 if let Some(pin_a) = snap_a.pins.iter().find(|p| p.id == pin_b.id) {
138 if pin_a.version != pin_b.version {
139 diffs.push(SnapshotDiff {
140 id: pin_b.id.clone(),
141 change: DiffChange::Changed {
142 from_version: pin_a.version.clone(),
143 to_version: pin_b.version.clone(),
144 },
145 });
146 }
147 } else {
148 diffs.push(SnapshotDiff {
149 id: pin_b.id.clone(),
150 change: DiffChange::Added {
151 version: pin_b.version.clone(),
152 },
153 });
154 }
155 }
156
157 for pin_a in &snap_a.pins {
159 if !snap_b.pins.iter().any(|p| p.id == pin_a.id) {
160 diffs.push(SnapshotDiff {
161 id: pin_a.id.clone(),
162 change: DiffChange::Removed {
163 version: pin_a.version.clone(),
164 },
165 });
166 }
167 }
168
169 Ok(diffs)
170}
171
172pub fn list_snapshots() -> Vec<(String, String)> {
174 let dir = match snapshots_dir() {
175 Some(d) if d.exists() => d,
176 _ => return vec![],
177 };
178
179 let entries = match fs::read_dir(&dir) {
180 Ok(e) => e,
181 Err(_) => return vec![],
182 };
183
184 let mut snapshots = Vec::new();
185 for entry in entries.filter_map(|e| e.ok()) {
186 let path = entry.path();
187 let ext = path.extension().and_then(|e| e.to_str());
188 if ext != Some("yaml") && ext != Some("yml") {
189 continue;
190 }
191 let stem = path
192 .file_stem()
193 .unwrap_or_default()
194 .to_string_lossy()
195 .to_string();
196
197 let created_at = fs::read_to_string(&path)
198 .ok()
199 .and_then(|s| serde_yaml::from_str::<Snapshot>(&s).ok())
200 .map(|s| s.created_at)
201 .unwrap_or_default();
202
203 snapshots.push((stem, created_at));
204 }
205
206 snapshots.sort_by(|a, b| a.0.cmp(&b.0));
207 snapshots
208}