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],
options: walker::DiscoveryOptions,
) -> bool {
let threshold = last_indexed_at
.checked_sub(SKEW_MARGIN)
.unwrap_or(last_indexed_at);
let (candidates, content_only) =
walker::discover_files_with_options(project_root, DEFAULT_EXCLUDES, options);
for path in candidates.iter().chain(content_only.iter()) {
match path.metadata() {
Ok(meta) => match meta.modified() {
Ok(modified) if modified <= threshold => {}
Ok(_) => return true,
Err(error) => {
log::debug!(
"treating project as changed: failed to read mtime for {}: {error}",
path.display()
);
return true;
}
},
Err(error) => {
log::debug!(
"treating project as changed: failed to read metadata for {}: {error}",
path.display()
);
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)
}
fn default_options() -> walker::DiscoveryOptions {
walker::DiscoveryOptions::default()
}
#[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,
default_options()
));
}
#[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,
default_options()
));
}
#[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,
default_options()
));
}
#[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,
default_options()
));
}
#[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,
default_options()
));
let at_margin = mtime + SKEW_MARGIN;
assert!(!project_changed_since(
root,
at_margin,
&indexed,
default_options()
));
let beyond_margin = mtime + Duration::from_secs(3);
assert!(!project_changed_since(
root,
beyond_margin,
&indexed,
default_options()
));
}
#[test]
fn gitignored_new_files_follow_respect_gitignore_setting() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::create_dir(root.join(".git")).expect("git dir");
write_file(root, ".gitignore", b"ignored.rs\n");
let ignored = write_file(root, "ignored.rs", b"fn ignored() {}\n");
set_mtime(&ignored, 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,
walker::DiscoveryOptions {
respect_gitignore: true
}
));
assert!(project_changed_since(
root,
last,
&indexed,
walker::DiscoveryOptions {
respect_gitignore: false
}
));
}
}