fix: scope pre-walked paths to $path roots and apply syncback filters

Bugbot flagged two issues with the pre-walked orphan paths:
1. cache.is_file.keys() included shallow project-folder entries (README.md,
   rokit.toml, etc.) that would be incorrectly flagged as orphans.
   Fixed by filtering to only paths under walked $path roots.
2. syncbackRules.ignorePaths was not applied to pre-walked paths.
   Fixed by running is_valid_path() on the pre-walked set before use.
This commit is contained in:
Joe 2026-03-07 16:35:47 -06:00
parent 53c0567153
commit 3666c2a5e1
2 changed files with 46 additions and 40 deletions

View file

@ -349,46 +349,46 @@ impl ServeSession {
prefetch_start.elapsed() prefetch_start.elapsed()
); );
if !cache.is_file.is_empty() { if !cache.is_file.is_empty() {
let ignore_rules = root_project.path_ignore_rules(); // Only include paths under walked $path roots (not
let project_path = root_project.folder_location(); // the shallow project-folder entries like README.md).
let git_glob = crate::glob::Glob::new(".git/**").unwrap(); // No syncback-specific filtering is applied here;
let has_hidden_component = |p: &Path| -> bool { // syncback_loop applies is_valid_path() per-path
if let Some(parent) = p.parent() { // which handles syncbackRules.ignorePaths.
for component in parent.components() { let roots = &cache.walked_roots;
if !roots.is_empty() {
let has_hidden_ancestor = |p: &Path, root: &Path| -> bool {
if let Ok(rel) = p.strip_prefix(root) {
for component in rel.components() {
if let Some(name) = component.as_os_str().to_str() { if let Some(name) = component.as_os_str().to_str() {
if name.starts_with('.') {
return true;
}
}
}
}
if let Some(name) = p.file_name().and_then(|n| n.to_str()) {
if name.starts_with('.') && name != ".gitkeep" { if name.starts_with('.') && name != ".gitkeep" {
return true; return true;
} }
} }
}
}
false false
}; };
let paths: HashSet<PathBuf> = cache let paths: HashSet<PathBuf> = cache
.is_file .is_file
.keys() .keys()
.filter(|p| { .filter(|p| {
let test_path = p.strip_prefix(project_path).unwrap_or(p); roots.iter().any(|root| {
if git_glob.is_match(test_path) { p.starts_with(root) && !has_hidden_ancestor(p, root)
return false; })
} })
if has_hidden_component(test_path) { .filter(|p| {
return false; // Exclude root entries themselves (depth 0)
} !roots.contains(p)
ignore_rules.iter().all(|rule| rule.passes(p))
}) })
.cloned() .cloned()
.collect(); .collect();
log::info!( log::info!(
"[PERF] captured {} walked paths from prefetch for orphan reuse", "[PERF] captured {} walked paths from prefetch ({} roots) for orphan reuse",
paths.len() paths.len(),
roots.len()
); );
walked_paths = Some(paths); walked_paths = Some(paths);
}
vfs.set_prefetch_cache(cache); vfs.set_prefetch_cache(cache);
} }
} }

View file

@ -290,11 +290,17 @@ pub fn syncback_loop_with_stats(
let phase_timer = std::time::Instant::now(); let phase_timer = std::time::Instant::now();
let existing_paths: HashSet<PathBuf> = if !incremental { let existing_paths: HashSet<PathBuf> = if !incremental {
if let Some(pre_walked) = pre_walked_paths.filter(|p| p.len() > 100) { if let Some(pre_walked) = pre_walked_paths.filter(|p| p.len() > 100) {
let before = pre_walked.len();
let filtered: HashSet<PathBuf> = pre_walked
.into_iter()
.filter(|p| is_valid_path(&ignore_patterns, project_path, p))
.collect();
log::info!( log::info!(
"[PERF] orphan scan: reusing {} pre-walked paths", "[PERF] orphan scan: reusing pre-walked paths ({} -> {} after syncback filters)",
pre_walked.len() before,
filtered.len()
); );
pre_walked filtered
} else { } else {
let mut paths = HashSet::new(); let mut paths = HashSet::new();