use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct InMemoryRollback {
entries: Vec<RollbackEntry>,
}
#[derive(Debug, Clone)]
struct RollbackEntry {
path: PathBuf,
original: Option<Vec<u8>>,
}
impl InMemoryRollback {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn capture(&mut self, path: &Path) {
if self.entries.iter().any(|e| e.path == path) {
return;
}
let original = if path.is_file() {
std::fs::read(path).ok()
} else {
None
};
self.entries.push(RollbackEntry {
path: path.to_path_buf(),
original,
});
}
pub fn restore_all(&self) {
for entry in &self.entries {
match &entry.original {
Some(content) => {
let _ = std::fs::write(&entry.path, content);
}
None => {
let _ = std::fs::remove_file(&entry.path);
}
}
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn test_root(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("homeboy-undo-{}-{}", name, nanos))
}
#[test]
fn in_memory_rollback_restores_modified_file() {
let root = test_root("imr-restore");
fs::create_dir_all(&root).unwrap();
let file = root.join("a.rs");
fs::write(&file, "original\n").unwrap();
let mut rollback = InMemoryRollback::new();
rollback.capture(&file);
assert_eq!(rollback.len(), 1);
fs::write(&file, "modified\n").unwrap();
assert!(fs::read_to_string(&file).unwrap().contains("modified"));
rollback.restore_all();
assert_eq!(fs::read_to_string(&file).unwrap(), "original\n");
let _ = fs::remove_dir_all(root);
}
#[test]
fn in_memory_rollback_removes_created_file() {
let root = test_root("imr-remove");
fs::create_dir_all(&root).unwrap();
let file = root.join("new.rs");
let mut rollback = InMemoryRollback::new();
rollback.capture(&file);
fs::write(&file, "created\n").unwrap();
assert!(file.exists());
rollback.restore_all();
assert!(!file.exists());
let _ = fs::remove_dir_all(root);
}
#[test]
fn in_memory_rollback_deduplicates() {
let root = test_root("imr-dedup");
fs::create_dir_all(&root).unwrap();
let file = root.join("dup.rs");
fs::write(&file, "content\n").unwrap();
let mut rollback = InMemoryRollback::new();
rollback.capture(&file);
rollback.capture(&file); assert_eq!(rollback.len(), 1);
let _ = fs::remove_dir_all(root);
}
#[test]
fn in_memory_rollback_handles_mixed_files() {
let root = test_root("imr-mixed");
fs::create_dir_all(&root).unwrap();
let existing = root.join("existing.rs");
let new_file = root.join("new.rs");
fs::write(&existing, "before\n").unwrap();
let mut rollback = InMemoryRollback::new();
rollback.capture(&existing);
rollback.capture(&new_file);
assert_eq!(rollback.len(), 2);
fs::write(&existing, "after\n").unwrap();
fs::write(&new_file, "created\n").unwrap();
rollback.restore_all();
assert_eq!(fs::read_to_string(&existing).unwrap(), "before\n");
assert!(!new_file.exists());
let _ = fs::remove_dir_all(root);
}
}