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