use std::path::Path;
use std::time::{Duration, SystemTime};
use crate::index::walker;
use super::util::DEFAULT_EXCLUDES;
const SKEW_MARGIN: Duration = Duration::from_secs(2);
pub fn project_changed_since(
project_root: &Path,
last_indexed_at: SystemTime,
indexed_paths: &[String],
) -> bool {
let threshold = last_indexed_at
.checked_sub(SKEW_MARGIN)
.unwrap_or(last_indexed_at);
let excludes: Vec<String> = DEFAULT_EXCLUDES.iter().map(|s| s.to_string()).collect();
let (candidates, content_only) = walker::discover_files(project_root, &excludes);
for path in candidates.iter().chain(content_only.iter()) {
match path.metadata().and_then(|meta| meta.modified()) {
Ok(modified) if modified <= threshold => {}
_ => return true,
}
}
indexed_paths
.iter()
.any(|rel| !project_root.join(rel).exists())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::path::PathBuf;
fn write_file(root: &Path, rel: &str, contents: &[u8]) -> PathBuf {
let path = root.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent");
}
std::fs::write(&path, contents).expect("write file");
path
}
fn set_mtime(path: &Path, time: SystemTime) {
File::options()
.write(true)
.open(path)
.expect("open file to set mtime")
.set_modified(time)
.expect("set mtime");
}
fn base_time() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000)
}
#[test]
fn reports_no_change_when_everything_predates_last_index() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
let readme = write_file(root, "README.md", b"# Title\n");
let base = base_time();
set_mtime(&lib, base);
set_mtime(&readme, base);
let last = base + Duration::from_secs(3600);
let indexed = vec!["src/lib.rs".to_string(), "README.md".to_string()];
assert!(!project_changed_since(root, last, &indexed));
}
#[test]
fn reports_change_when_a_file_is_modified_after_last_index() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
set_mtime(&lib, base_time() + Duration::from_secs(7200));
let last = base_time() + Duration::from_secs(3600);
let indexed = vec!["src/lib.rs".to_string()];
assert!(project_changed_since(root, last, &indexed));
}
#[test]
fn reports_change_for_newly_added_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let added = write_file(root, "src/new.rs", b"fn added() {}\n");
set_mtime(&added, base_time() + Duration::from_secs(7200));
let last = base_time() + Duration::from_secs(3600);
let indexed: Vec<String> = Vec::new();
assert!(project_changed_since(root, last, &indexed));
}
#[test]
fn reports_change_when_indexed_file_is_deleted() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
set_mtime(&lib, base_time());
let last = base_time() + Duration::from_secs(3600);
let indexed = vec!["src/lib.rs".to_string(), "src/gone.rs".to_string()];
assert!(project_changed_since(root, last, &indexed));
}
#[test]
fn skew_margin_boundary_only_ever_makes_the_gate_more_eager() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
let mtime = base_time();
set_mtime(&lib, mtime);
let indexed = vec!["src/lib.rs".to_string()];
let within_margin = mtime + Duration::from_secs(1);
assert!(project_changed_since(root, within_margin, &indexed));
let at_margin = mtime + SKEW_MARGIN;
assert!(!project_changed_since(root, at_margin, &indexed));
let beyond_margin = mtime + Duration::from_secs(3);
assert!(!project_changed_since(root, beyond_margin, &indexed));
}
}