use crate::error::Result;
use crate::paths::Paths;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HistoryAction {
Install,
Upgrade,
Downgrade,
Reinstall,
Uninstall,
}
impl HistoryAction {
pub fn as_str(&self) -> &'static str {
match self {
Self::Install => "install",
Self::Upgrade => "upgrade",
Self::Downgrade => "downgrade",
Self::Reinstall => "reinstall",
Self::Uninstall => "uninstall",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub version: String,
pub revision: u32,
pub action: HistoryAction,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_revision: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bottle_path: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct PackageHistory {
#[serde(default)]
pub packages: HashMap<String, Vec<HistoryEntry>>,
}
impl PackageHistory {
pub fn load(paths: &Paths) -> Result<Self> {
let file_path = paths.history_file();
if file_path.exists() {
let contents = std::fs::read_to_string(&file_path)?;
let history: PackageHistory = serde_json::from_str(&contents)?;
Ok(history)
} else {
Ok(Self::default())
}
}
pub fn save(&self, paths: &Paths) -> Result<()> {
let file_path = paths.history_file();
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)?;
}
let contents = serde_json::to_string_pretty(self)?;
std::fs::write(&file_path, contents)?;
Ok(())
}
pub fn record_install(&mut self, name: &str, version: &str, revision: u32) {
self.record(name, version, revision, HistoryAction::Install, None, None);
}
pub fn record_upgrade(
&mut self,
name: &str,
version: &str,
revision: u32,
from_version: &str,
from_revision: u32,
) {
self.record(
name,
version,
revision,
HistoryAction::Upgrade,
Some(from_version.to_string()),
Some(from_revision),
);
}
pub fn record_downgrade(
&mut self,
name: &str,
version: &str,
revision: u32,
from_version: &str,
from_revision: u32,
) {
self.record(
name,
version,
revision,
HistoryAction::Downgrade,
Some(from_version.to_string()),
Some(from_revision),
);
}
pub fn record_reinstall(&mut self, name: &str, version: &str, revision: u32) {
self.record(
name,
version,
revision,
HistoryAction::Reinstall,
None,
None,
);
}
pub fn record_uninstall(&mut self, name: &str, version: &str, revision: u32) {
self.record(
name,
version,
revision,
HistoryAction::Uninstall,
None,
None,
);
}
fn record(
&mut self,
name: &str,
version: &str,
revision: u32,
action: HistoryAction,
from_version: Option<String>,
from_revision: Option<u32>,
) {
let entry = HistoryEntry {
version: version.to_string(),
revision,
action,
timestamp: chrono_lite_now(),
from_version,
from_revision,
bottle_path: None,
};
self.packages
.entry(name.to_string())
.or_default()
.push(entry);
}
pub fn get(&self, name: &str) -> Option<&Vec<HistoryEntry>> {
self.packages.get(name)
}
pub fn get_latest(&self, name: &str) -> Option<&HistoryEntry> {
self.packages.get(name).and_then(|entries| entries.last())
}
pub fn get_previous(&self, name: &str) -> Option<&HistoryEntry> {
self.packages.get(name).and_then(|entries| {
if entries.len() >= 2 {
entries
.iter()
.rev()
.skip(1)
.find(|e| e.action != HistoryAction::Uninstall)
} else {
None
}
})
}
pub fn get_installed_versions(&self, name: &str) -> Vec<(String, u32)> {
self.packages
.get(name)
.map(|entries| {
let mut versions: Vec<(String, u32)> = entries
.iter()
.filter(|e| e.action != HistoryAction::Uninstall)
.map(|e| (e.version.clone(), e.revision))
.collect();
versions.dedup();
versions
})
.unwrap_or_default()
}
pub fn has_history(&self, name: &str) -> bool {
self.packages
.get(name)
.map(|e| !e.is_empty())
.unwrap_or(false)
}
pub fn prune(&mut self, keep: usize) {
for entries in self.packages.values_mut() {
if entries.len() > keep {
let start = entries.len() - keep;
*entries = entries.drain(start..).collect();
}
}
}
pub fn remove(&mut self, name: &str) {
self.packages.remove(name);
}
}
fn chrono_lite_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let days_since_epoch = secs / 86400;
let remaining_secs = secs % 86400;
let hours = remaining_secs / 3600;
let minutes = (remaining_secs % 3600) / 60;
let seconds = remaining_secs % 60;
let years = 1970 + (days_since_epoch / 365);
let day_of_year = days_since_epoch % 365;
let month = (day_of_year / 30).min(11) + 1;
let day = (day_of_year % 30) + 1;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
years, month, day, hours, minutes, seconds
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_history() {
let mut history = PackageHistory::default();
history.record_install("jq", "1.7", 0);
history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
let entries = history.get("jq").unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].action, HistoryAction::Install);
assert_eq!(entries[1].action, HistoryAction::Upgrade);
assert_eq!(entries[1].from_version, Some("1.7".to_string()));
}
#[test]
fn test_get_previous() {
let mut history = PackageHistory::default();
history.record_install("jq", "1.6", 0);
history.record_upgrade("jq", "1.7", 0, "1.6", 0);
history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
let prev = history.get_previous("jq").unwrap();
assert_eq!(prev.version, "1.7");
}
#[test]
fn test_prune() {
let mut history = PackageHistory::default();
history.record_install("jq", "1.5", 0);
history.record_upgrade("jq", "1.6", 0, "1.5", 0);
history.record_upgrade("jq", "1.7", 0, "1.6", 0);
history.record_upgrade("jq", "1.7.1", 0, "1.7", 0);
history.prune(2);
let entries = history.get("jq").unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].version, "1.7");
assert_eq!(entries[1].version, "1.7.1");
}
}