gobby_code/index/indexer/
freshness_probe.rs1use std::path::Path;
12use std::time::{Duration, SystemTime};
13
14use crate::index::walker;
15
16use super::util::DEFAULT_EXCLUDES;
17
18const SKEW_MARGIN: Duration = Duration::from_secs(2);
25
26pub fn project_changed_since(
38 project_root: &Path,
39 last_indexed_at: SystemTime,
40 indexed_paths: &[String],
41) -> bool {
42 let threshold = last_indexed_at
43 .checked_sub(SKEW_MARGIN)
44 .unwrap_or(last_indexed_at);
45
46 let (candidates, content_only) = walker::discover_files(project_root, DEFAULT_EXCLUDES);
47
48 for path in candidates.iter().chain(content_only.iter()) {
53 match path.metadata() {
54 Ok(meta) => match meta.modified() {
55 Ok(modified) if modified <= threshold => {}
56 Ok(_) => return true,
57 Err(error) => {
58 log::debug!(
59 "treating project as changed: failed to read mtime for {}: {error}",
60 path.display()
61 );
62 return true;
63 }
64 },
65 Err(error) => {
66 log::debug!(
67 "treating project as changed: failed to read metadata for {}: {error}",
68 path.display()
69 );
70 return true;
71 }
72 }
73 }
74
75 indexed_paths
77 .iter()
78 .any(|rel| !project_root.join(rel).exists())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::fs::File;
85 use std::path::PathBuf;
86
87 fn write_file(root: &Path, rel: &str, contents: &[u8]) -> PathBuf {
88 let path = root.join(rel);
89 if let Some(parent) = path.parent() {
90 std::fs::create_dir_all(parent).expect("create parent");
91 }
92 std::fs::write(&path, contents).expect("write file");
93 path
94 }
95
96 fn set_mtime(path: &Path, time: SystemTime) {
97 File::options()
98 .write(true)
99 .open(path)
100 .expect("open file to set mtime")
101 .set_modified(time)
102 .expect("set mtime");
103 }
104
105 fn base_time() -> SystemTime {
108 SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000)
109 }
110
111 #[test]
112 fn reports_no_change_when_everything_predates_last_index() {
113 let tmp = tempfile::tempdir().expect("tempdir");
114 let root = tmp.path();
115 let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
116 let readme = write_file(root, "README.md", b"# Title\n");
117
118 let base = base_time();
119 set_mtime(&lib, base);
120 set_mtime(&readme, base);
121
122 let last = base + Duration::from_secs(3600);
124 let indexed = vec!["src/lib.rs".to_string(), "README.md".to_string()];
125
126 assert!(!project_changed_since(root, last, &indexed));
127 }
128
129 #[test]
130 fn reports_change_when_a_file_is_modified_after_last_index() {
131 let tmp = tempfile::tempdir().expect("tempdir");
132 let root = tmp.path();
133 let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
134 set_mtime(&lib, base_time() + Duration::from_secs(7200));
135
136 let last = base_time() + Duration::from_secs(3600);
137 let indexed = vec!["src/lib.rs".to_string()];
138
139 assert!(project_changed_since(root, last, &indexed));
140 }
141
142 #[test]
143 fn reports_change_for_newly_added_file() {
144 let tmp = tempfile::tempdir().expect("tempdir");
147 let root = tmp.path();
148 let added = write_file(root, "src/new.rs", b"fn added() {}\n");
149 set_mtime(&added, base_time() + Duration::from_secs(7200));
150
151 let last = base_time() + Duration::from_secs(3600);
152 let indexed: Vec<String> = Vec::new();
153
154 assert!(project_changed_since(root, last, &indexed));
155 }
156
157 #[test]
158 fn reports_change_when_indexed_file_is_deleted() {
159 let tmp = tempfile::tempdir().expect("tempdir");
160 let root = tmp.path();
161 let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
162 set_mtime(&lib, base_time());
163
164 let last = base_time() + Duration::from_secs(3600);
165 let indexed = vec!["src/lib.rs".to_string(), "src/gone.rs".to_string()];
167
168 assert!(project_changed_since(root, last, &indexed));
169 }
170
171 #[test]
172 fn skew_margin_boundary_only_ever_makes_the_gate_more_eager() {
173 let tmp = tempfile::tempdir().expect("tempdir");
174 let root = tmp.path();
175 let lib = write_file(root, "src/lib.rs", b"fn main() {}\n");
176 let mtime = base_time();
177 set_mtime(&lib, mtime);
178 let indexed = vec!["src/lib.rs".to_string()];
179
180 let within_margin = mtime + Duration::from_secs(1);
183 assert!(project_changed_since(root, within_margin, &indexed));
184
185 let at_margin = mtime + SKEW_MARGIN;
188 assert!(!project_changed_since(root, at_margin, &indexed));
189
190 let beyond_margin = mtime + Duration::from_secs(3);
193 assert!(!project_changed_since(root, beyond_margin, &indexed));
194 }
195}