use crate::models::changes::Change;
use crate::models::common::Version;
use crate::models::tasks::Task;
use anyhow::{Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};
pub struct HistoryStorage {
entries: Vec<HistoryEntry>,
history_file: PathBuf,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HistoryEntry {
pub change: Change,
pub version: Option<Version>,
}
impl HistoryEntry {
pub fn from_task(task: &Task) -> Self {
Self {
change: Change::from(task),
version: task.completed_at_version.clone(),
}
}
}
impl HistoryStorage {
pub fn new(history_file: &Path) -> Result<Self> {
let mut storage = Self {
entries: Vec::new(),
history_file: history_file.to_path_buf(),
};
storage.load()?;
Ok(storage)
}
fn load(&mut self) -> Result<()> {
if !self.history_file.exists() {
self.entries = Vec::new();
return Ok(());
}
let content = fs::read_to_string(&self.history_file)?;
self.entries = serde_json::from_str(&content).unwrap_or_default();
Ok(())
}
fn save(&self) -> Result<()> {
if let Some(parent) = self.history_file.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(&self.entries)
.map_err(|e| anyhow!("Failed to serialize history: {}", e))?;
fs::write(&self.history_file, json)
.map_err(|e| anyhow!("Failed to write history file: {}", e))?;
Ok(())
}
fn is_same_entry(a: &HistoryEntry, b: &HistoryEntry) -> bool {
match (&a.change.commit, &b.change.commit) {
(Some(left), Some(right)) => left == right,
_ => a.change.description == b.change.description && a.version == b.version,
}
}
fn insert_if_new(&mut self, task: &Task) {
let candidate = HistoryEntry::from_task(task);
let already_exists = self
.entries
.iter()
.any(|entry| Self::is_same_entry(entry, &candidate));
if !already_exists {
self.entries.push(candidate);
}
}
pub fn record(&mut self, task: &Task) -> Result<()> {
if !task.completed {
return Ok(());
}
self.insert_if_new(task);
self.save()
}
pub fn record_all(&mut self, tasks: &[&Task]) -> Result<()> {
for task in tasks {
if !task.completed {
continue;
}
self.insert_if_new(task);
}
self.save()
}
pub fn assign_version(&mut self, version: &Version) -> Result<usize> {
let mut count = 0;
for entry in &mut self.entries {
if entry.version.is_none() {
entry.version = Some(version.clone());
count += 1;
}
}
if count > 0 {
self.save()?;
}
Ok(count)
}
pub fn entries_for_version(&self, version: &Version) -> Vec<&HistoryEntry> {
self.entries
.iter()
.filter(|e| e.version.as_ref() == Some(version))
.collect()
}
pub fn entries_by_version(&self) -> std::collections::BTreeMap<Version, Vec<&HistoryEntry>> {
let mut map = std::collections::BTreeMap::new();
for entry in &self.entries {
if let Some(version) = &entry.version {
map.entry(version.clone())
.or_insert_with(Vec::new)
.push(entry);
}
}
map
}
}
#[cfg(test)]
#[path = "../../../tests/services/storage/history_storage_tests.rs"]
mod tests;