use std::collections::BTreeMap;
use std::path::Path;
use std::time::UNIX_EPOCH;
use crate::config::Config;
use crate::scanner;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Snapshot {
files: BTreeMap<String, u128>,
}
impl Snapshot {
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Changes {
pub added: Vec<String>,
pub modified: Vec<String>,
pub removed: Vec<String>,
}
impl Changes {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.modified.is_empty() && self.removed.is_empty()
}
pub fn count(&self) -> usize {
self.added.len() + self.modified.len() + self.removed.len()
}
pub fn all_paths(&self) -> Vec<&str> {
let mut v: Vec<&str> = self
.added
.iter()
.chain(&self.modified)
.chain(&self.removed)
.map(String::as_str)
.collect();
v.sort_unstable();
v
}
}
pub fn snapshot(root: &Path, config: &Config) -> Snapshot {
let scan = scanner::scan(root, config);
let mut files = BTreeMap::new();
for file in &scan.files {
if let Some(mtime) = mtime_nanos(&root.join(&file.path)) {
files.insert(file.path.clone(), mtime);
}
}
Snapshot { files }
}
pub fn diff(old: &Snapshot, new: &Snapshot) -> Changes {
let mut changes = Changes::default();
for (path, mtime) in &new.files {
match old.files.get(path) {
None => changes.added.push(path.clone()),
Some(prev) if prev != mtime => changes.modified.push(path.clone()),
Some(_) => {}
}
}
for path in old.files.keys() {
if !new.files.contains_key(path) {
changes.removed.push(path.clone());
}
}
changes.added.sort();
changes.modified.sort();
changes.removed.sort();
changes
}
fn mtime_nanos(path: &Path) -> Option<u128> {
let modified = std::fs::metadata(path).ok()?.modified().ok()?;
Some(modified.duration_since(UNIX_EPOCH).ok()?.as_nanos())
}
#[cfg(test)]
mod tests {
use super::*;
fn snap(entries: &[(&str, u128)]) -> Snapshot {
Snapshot {
files: entries.iter().map(|(p, m)| (p.to_string(), *m)).collect(),
}
}
#[test]
fn no_changes_when_identical() {
let a = snap(&[("src/main.rs", 1), ("src/lib.rs", 2)]);
let b = a.clone();
assert!(diff(&a, &b).is_empty());
}
#[test]
fn detects_added_modified_removed() {
let old = snap(&[("a.rs", 1), ("b.rs", 2), ("c.rs", 3)]);
let new = snap(&[("a.rs", 1), ("b.rs", 9), ("d.rs", 4)]);
let changes = diff(&old, &new);
assert_eq!(changes.added, vec!["d.rs"]);
assert_eq!(changes.modified, vec!["b.rs"]);
assert_eq!(changes.removed, vec!["c.rs"]);
assert_eq!(changes.count(), 3);
assert!(!changes.is_empty());
}
#[test]
fn snapshot_reads_source_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("main.rs"), "fn main() {}").unwrap();
std::fs::create_dir(dir.path().join("target")).unwrap();
std::fs::write(dir.path().join("target").join("junk.rs"), "// ignored").unwrap();
let snap = snapshot(dir.path(), &Config::default());
assert_eq!(snap.len(), 1);
assert!(!snap.is_empty());
}
}