1use crate::error::Result;
4use crate::paths::Paths;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum HistoryAction {
12 Install,
13 Upgrade,
14 Downgrade,
15 Reinstall,
16 Uninstall,
17}
18
19impl HistoryAction {
20 pub fn as_str(&self) -> &'static str {
21 match self {
22 Self::Install => "install",
23 Self::Upgrade => "upgrade",
24 Self::Downgrade => "downgrade",
25 Self::Reinstall => "reinstall",
26 Self::Uninstall => "uninstall",
27 }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct HistoryEntry {
34 pub version: String,
35 pub revision: u32,
36 pub action: HistoryAction,
37 pub timestamp: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub from_version: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub from_revision: Option<u32>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub bottle_path: Option<String>,
44}
45
46#[derive(Debug, Default, Clone, Serialize, Deserialize)]
48pub struct PackageHistory {
49 #[serde(default)]
50 pub packages: HashMap<String, Vec<HistoryEntry>>,
51}
52
53impl PackageHistory {
54 pub fn load(paths: &Paths) -> Result<Self> {
56 let file_path = paths.history_file();
57
58 if file_path.exists() {
59 let contents = std::fs::read_to_string(&file_path)?;
60 let history: PackageHistory = serde_json::from_str(&contents)?;
61 Ok(history)
62 } else {
63 Ok(Self::default())
64 }
65 }
66
67 pub fn save(&self, paths: &Paths) -> Result<()> {
69 let file_path = paths.history_file();
70
71 if let Some(parent) = file_path.parent() {
72 std::fs::create_dir_all(parent)?;
73 }
74
75 let contents = serde_json::to_string_pretty(self)?;
76 std::fs::write(&file_path, contents)?;
77 Ok(())
78 }
79
80 pub fn record_install(&mut self, name: &str, version: &str, revision: u32) {
82 self.record(name, version, revision, HistoryAction::Install, None, None);
83 }
84
85 pub fn record_upgrade(
87 &mut self,
88 name: &str,
89 version: &str,
90 revision: u32,
91 from_version: &str,
92 from_revision: u32,
93 ) {
94 self.record(
95 name,
96 version,
97 revision,
98 HistoryAction::Upgrade,
99 Some(from_version.to_string()),
100 Some(from_revision),
101 );
102 }
103
104 pub fn record_downgrade(
106 &mut self,
107 name: &str,
108 version: &str,
109 revision: u32,
110 from_version: &str,
111 from_revision: u32,
112 ) {
113 self.record(
114 name,
115 version,
116 revision,
117 HistoryAction::Downgrade,
118 Some(from_version.to_string()),
119 Some(from_revision),
120 );
121 }
122
123 pub fn record_reinstall(&mut self, name: &str, version: &str, revision: u32) {
125 self.record(
126 name,
127 version,
128 revision,
129 HistoryAction::Reinstall,
130 None,
131 None,
132 );
133 }
134
135 pub fn record_uninstall(&mut self, name: &str, version: &str, revision: u32) {
137 self.record(
138 name,
139 version,
140 revision,
141 HistoryAction::Uninstall,
142 None,
143 None,
144 );
145 }
146
147 fn record(
149 &mut self,
150 name: &str,
151 version: &str,
152 revision: u32,
153 action: HistoryAction,
154 from_version: Option<String>,
155 from_revision: Option<u32>,
156 ) {
157 let entry = HistoryEntry {
158 version: version.to_string(),
159 revision,
160 action,
161 timestamp: chrono_lite_now(),
162 from_version,
163 from_revision,
164 bottle_path: None,
165 };
166
167 self.packages
168 .entry(name.to_string())
169 .or_default()
170 .push(entry);
171 }
172
173 pub fn get(&self, name: &str) -> Option<&Vec<HistoryEntry>> {
175 self.packages.get(name)
176 }
177
178 pub fn get_latest(&self, name: &str) -> Option<&HistoryEntry> {
180 self.packages.get(name).and_then(|entries| entries.last())
181 }
182
183 pub fn get_previous(&self, name: &str) -> Option<&HistoryEntry> {
185 self.packages.get(name).and_then(|entries| {
186 if entries.len() >= 2 {
187 entries
189 .iter()
190 .rev()
191 .skip(1)
192 .find(|e| e.action != HistoryAction::Uninstall)
193 } else {
194 None
195 }
196 })
197 }
198
199 pub fn get_installed_versions(&self, name: &str) -> Vec<(String, u32)> {
201 self.packages
202 .get(name)
203 .map(|entries| {
204 let mut versions: Vec<(String, u32)> = entries
205 .iter()
206 .filter(|e| e.action != HistoryAction::Uninstall)
207 .map(|e| (e.version.clone(), e.revision))
208 .collect();
209 versions.dedup();
210 versions
211 })
212 .unwrap_or_default()
213 }
214
215 pub fn has_history(&self, name: &str) -> bool {
217 self.packages
218 .get(name)
219 .map(|e| !e.is_empty())
220 .unwrap_or(false)
221 }
222
223 pub fn prune(&mut self, keep: usize) {
225 for entries in self.packages.values_mut() {
226 if entries.len() > keep {
227 let start = entries.len() - keep;
228 *entries = entries.drain(start..).collect();
229 }
230 }
231 }
232
233 pub fn remove(&mut self, name: &str) {
235 self.packages.remove(name);
236 }
237}
238
239fn chrono_lite_now() -> String {
241 use std::time::{SystemTime, UNIX_EPOCH};
242
243 let duration = SystemTime::now()
244 .duration_since(UNIX_EPOCH)
245 .unwrap_or_default();
246
247 let secs = duration.as_secs();
248
249 let days_since_epoch = secs / 86400;
251 let remaining_secs = secs % 86400;
252 let hours = remaining_secs / 3600;
253 let minutes = (remaining_secs % 3600) / 60;
254 let seconds = remaining_secs % 60;
255
256 let years = 1970 + (days_since_epoch / 365);
258 let day_of_year = days_since_epoch % 365;
259 let month = (day_of_year / 30).min(11) + 1;
260 let day = (day_of_year % 30) + 1;
261
262 format!(
263 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
264 years, month, day, hours, minutes, seconds
265 )
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_record_history() {
274 let mut history = PackageHistory::default();
275
276 history.record_install("jq", "1.7", 0);
277 history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
278
279 let entries = history.get("jq").unwrap();
280 assert_eq!(entries.len(), 2);
281 assert_eq!(entries[0].action, HistoryAction::Install);
282 assert_eq!(entries[1].action, HistoryAction::Upgrade);
283 assert_eq!(entries[1].from_version, Some("1.7".to_string()));
284 }
285
286 #[test]
287 fn test_get_previous() {
288 let mut history = PackageHistory::default();
289
290 history.record_install("jq", "1.6", 0);
291 history.record_upgrade("jq", "1.7", 0, "1.6", 0);
292 history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
293
294 let prev = history.get_previous("jq").unwrap();
295 assert_eq!(prev.version, "1.7");
296 }
297
298 #[test]
299 fn test_prune() {
300 let mut history = PackageHistory::default();
301
302 history.record_install("jq", "1.5", 0);
303 history.record_upgrade("jq", "1.6", 0, "1.5", 0);
304 history.record_upgrade("jq", "1.7", 0, "1.6", 0);
305 history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
306
307 history.prune(2);
308
309 let entries = history.get("jq").unwrap();
310 assert_eq!(entries.len(), 2);
311 assert_eq!(entries[0].version, "1.7");
312 assert_eq!(entries[1].version, "1.7.1");
313 }
314}