From cd9b480fddc7633995c1bb84cfdededf6fcce999 Mon Sep 17 00:00:00 2001 From: Yunfan Yang Date: Tue, 2 Jun 2026 23:13:27 -0400 Subject: [PATCH 1/3] Stop watching gitignored directories in the repo file watcher Make the repository file watcher's descend predicate gitignore-aware so we no longer register inotify watches on gitignored directories (node_modules, build output, vendored deps), while still watching registered ignored-path interests (e.g. skill provider dirs). Fixes excessive memory usage observed on large monorepos. Co-Authored-By: Warp --- app/src/lib.rs | 13 ++ crates/repo_metadata/src/entry.rs | 72 +++++++++-- crates/repo_metadata/src/entry_tests.rs | 156 +++++++++++++++++++++++- crates/repo_metadata/src/local_model.rs | 9 +- crates/repo_metadata/src/watcher.rs | 31 ++++- 5 files changed, 270 insertions(+), 11 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index eebccadb65..eb83ffb2dd 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1563,6 +1563,19 @@ pub(crate) fn initialize_app( model }); + + // Mirror the skill interests onto the DirectoryWatcher so the + // gitignore-pruning descend filter still watches gitignored skill + // directories for `Repository` subscribers (LSP, MCP). Registered at + // startup, before any repository begins watching, so it gates descent + // on the very first registration. + DirectoryWatcher::handle(ctx).update(ctx, |watcher, _| { + watcher.register_ignored_path_interests( + ::ai::skills::SKILL_PROVIDER_DEFINITIONS + .iter() + .map(|provider| provider.skills_path.clone()), + ); + }); } { diff --git a/crates/repo_metadata/src/entry.rs b/crates/repo_metadata/src/entry.rs index 0f130707f7..03cb4a4b17 100644 --- a/crates/repo_metadata/src/entry.rs +++ b/crates/repo_metadata/src/entry.rs @@ -910,21 +910,79 @@ fn descend_allowlist_matches(suffix: &[Component<'_>]) -> bool { } } +/// Returns whether a repository file watcher should descend into (and register +/// a watch on) `path`. +/// +/// This is the descend predicate of [`repo_watch_filter`]. Precedence: +/// 1. `.git/` internals follow the existing allowlist +/// ([`should_watch_directory_in_git_path`]). +/// 2. Any directory on the path to — or inside — a registered ignored-path +/// interest is always watched, so consumers (e.g. skills) keep getting +/// updates for ignored subtrees they explicitly care about. This mirrors +/// the tree builder's lazy-load exception +/// ([`matches_ignored_path_interest`]) and, because that match also returns +/// `true` for the ancestor prefixes leading to an interest, it preserves +/// the watcher's monotonicity invariant. +/// 3. Otherwise, gitignored directories are pruned so we don't register +/// inotify watches on `node_modules`, build output, vendored deps, etc. +/// +/// Ancestor-aware matching (`check_ancestors = true`) is what makes pruning +/// safe w.r.t. the monotonicity invariant: a child of an ignored directory is +/// itself reported as ignored, so we never accept a descendant after +/// rejecting its parent. Directory-only re-include negations +/// (`parentdir/*` + `!parentdir/*/`) keep working because the negated +/// directory matches as not-ignored on its own path (last-match-wins via +/// `matched_path_or_any_parents`), so we still descend into it. +pub fn should_watch_repo_directory( + path: &Path, + gitignores: &[Gitignore], + ignored_path_interests: &[PathBuf], +) -> bool { + if is_git_internal_path(path) { + return should_watch_directory_in_git_path(path); + } + + if matches_ignored_path_interest(path, ignored_path_interests) { + return true; + } + + !matches_gitignores( + path, + path.is_dir(), + gitignores, + /* check_ancestors */ true, + ) +} + /// Returns the [`WatchFilter`] used by repository file watchers. /// /// Emit predicate: forwards events for everything outside `.git/` plus the /// allowlisted files inside `.git/` (HEAD, refs/heads/*, index.lock, /// config, config.worktree, refs/remotes//*, and worktree equivalents). +/// Gitignored files that live directly in a watched (non-ignored) directory +/// are still emitted here and tagged `is_ignored` downstream, preserving +/// existing behavior. +/// +/// Descend predicate: see [`should_watch_repo_directory`]. In addition to the +/// `.git/` allowlist, it prunes gitignored directories (honoring registered +/// ignored-path interests) so the recursive walk does not register watches on +/// gitignored subtrees. /// -/// Descend predicate: prunes `.git/objects/`, `.git/hooks/`, `.git/logs/`, -/// `.git/info/`, `.git/lfs/`, etc. so the recursive walk does not register -/// watches on those subtrees, but still descends into `.git/`, -/// `.git/refs/heads/`, `.git/refs/remotes//`, and `.git/worktrees//` -/// so the allowlisted children remain reachable on Linux. +/// `gitignores` should be the repo's root + global gitignores (as produced by +/// [`gitignores_for_directory`]), matching `Repository::check_gitignore_status` +/// so descend decisions and the downstream `is_ignored` tagging stay +/// consistent. Nested per-directory `.gitignore` files are not consulted here +/// (same limitation as the existing tagging), which can only cause us to +/// over-watch, never to miss events. #[cfg(feature = "local_fs")] -pub fn repo_watch_filter() -> WatchFilter { +pub fn repo_watch_filter( + gitignores: Vec, + ignored_path_interests: Vec, +) -> WatchFilter { + let should_watch = + move |path: &Path| should_watch_repo_directory(path, &gitignores, &ignored_path_interests); WatchFilter::with_filter( - Arc::new(should_watch_directory_in_git_path), + Arc::new(should_watch), Arc::new(|path: &Path| !should_ignore_git_path(path)), ) } diff --git a/crates/repo_metadata/src/entry_tests.rs b/crates/repo_metadata/src/entry_tests.rs index e104f4edd7..0e553ed3ef 100644 --- a/crates/repo_metadata/src/entry_tests.rs +++ b/crates/repo_metadata/src/entry_tests.rs @@ -2,7 +2,7 @@ use std::fs; use ignore::gitignore::Gitignore; -use super::{Entry, IgnoredPathStrategy}; +use super::{matches_gitignores, Entry, IgnoredPathStrategy}; #[test] fn test_git_path_filtering_allowlist() { use std::path::Path; @@ -173,6 +173,160 @@ fn test_git_path_filtering_allowlist() { } } +/// Writes a `.gitignore` with `content` at `root` and returns a [`Gitignore`] +/// rooted there. Uses only the repo-root gitignore (not the machine's global +/// gitignore) so tests are deterministic. +fn gitignore_rooted(root: &std::path::Path, content: &str) -> Gitignore { + fs::write(root.join(".gitignore"), content).unwrap(); + let (gitignore, _) = Gitignore::new(root.join(".gitignore")); + gitignore +} + +#[test] +fn should_watch_prunes_gitignored_directory() { + let temp_dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(temp_dir.path()).unwrap(); + fs::create_dir(root.join("node_modules")).unwrap(); + fs::create_dir(root.join("src")).unwrap(); + let gitignores = vec![gitignore_rooted(&root, "node_modules/\n")]; + + // Root and non-ignored dirs are watched; the gitignored dir is pruned. + assert!(super::should_watch_repo_directory(&root, &gitignores, &[])); + assert!(super::should_watch_repo_directory( + &root.join("src"), + &gitignores, + &[] + )); + assert!(!super::should_watch_repo_directory( + &root.join("node_modules"), + &gitignores, + &[] + )); + // Descendants of an ignored dir are also pruned (ancestor-aware), which is + // what preserves the watcher's monotonicity invariant. + assert!(!super::should_watch_repo_directory( + &root.join("node_modules/foo"), + &gitignores, + &[] + )); +} + +#[test] +fn should_watch_descends_to_interest_under_ignored_ancestor() { + let temp_dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(temp_dir.path()).unwrap(); + fs::create_dir_all(root.join(".agents/skills/test")).unwrap(); + fs::create_dir(root.join(".agents/other")).unwrap(); + let gitignores = vec![gitignore_rooted(&root, ".agents/\n")]; + let interests = vec![std::path::PathBuf::from(".agents/skills")]; + + // The whole `.agents` subtree is gitignored, but we still descend along the + // prefix to reach the registered interest, and into the interest subtree. + assert!(super::should_watch_repo_directory( + &root.join(".agents"), + &gitignores, + &interests + )); + assert!(super::should_watch_repo_directory( + &root.join(".agents/skills"), + &gitignores, + &interests + )); + assert!(super::should_watch_repo_directory( + &root.join(".agents/skills/test"), + &gitignores, + &interests + )); + // A sibling ignored dir that is not part of any interest is still pruned. + assert!(!super::should_watch_repo_directory( + &root.join(".agents/other"), + &gitignores, + &interests + )); +} + +#[test] +fn should_watch_handles_nested_ignored_ancestor_with_deeper_interest() { + let temp_dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(temp_dir.path()).unwrap(); + fs::create_dir_all(root.join("a/b/c")).unwrap(); + fs::create_dir(root.join("a/b/other")).unwrap(); + let gitignores = vec![gitignore_rooted(&root, "a/b/\n")]; + let interests = vec![std::path::PathBuf::from("a/b/c")]; + + // `a/b` is ignored but `a/b/c` is a registered interest: descend along the + // whole prefix and into the interest, while pruning the ignored sibling. + assert!(super::should_watch_repo_directory( + &root.join("a"), + &gitignores, + &interests + )); + assert!(super::should_watch_repo_directory( + &root.join("a/b"), + &gitignores, + &interests + )); + assert!(super::should_watch_repo_directory( + &root.join("a/b/c"), + &gitignores, + &interests + )); + assert!(!super::should_watch_repo_directory( + &root.join("a/b/other"), + &gitignores, + &interests + )); +} + +#[test] +fn should_watch_descends_dir_only_reinclude_negation() { + let temp_dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(temp_dir.path()).unwrap(); + fs::create_dir_all(root.join("parentdir/sub")).unwrap(); + fs::write(root.join("parentdir/loose.txt"), "").unwrap(); + // Ignore the loose files in `parentdir` but re-include its subdirectories. + let gitignores = vec![gitignore_rooted(&root, "parentdir/*\n!parentdir/*/\n")]; + + // `parentdir` itself is not matched by `parentdir/*`, so we descend. + assert!(super::should_watch_repo_directory( + &root.join("parentdir"), + &gitignores, + &[] + )); + // The subdirectory is re-included by the directory-only negation, so it is + // still watched even though `parentdir/*` matched it first. + assert!(super::should_watch_repo_directory( + &root.join("parentdir/sub"), + &gitignores, + &[] + )); + // The loose file remains gitignored (the negation is directory-only); the + // emit predicate filters it, but `parentdir` stays watched for its subdirs. + assert!(matches_gitignores( + &root.join("parentdir/loose.txt"), + false, + &gitignores, + true, + )); +} + +#[test] +fn should_watch_preserves_git_internal_allowlist() { + // No gitignores / interests needed: `.git` handling short-circuits and is + // path-based, mirroring `should_watch_directory_in_git_path`. + let repo = std::path::Path::new("/home/user/project"); + assert!(super::should_watch_repo_directory( + &repo.join(".git/refs/heads"), + &[], + &[] + )); + assert!(!super::should_watch_repo_directory( + &repo.join(".git/objects"), + &[], + &[] + )); +} + fn find_entry<'a>(entry: &'a super::Entry, path: &std::path::Path) -> Option<&'a super::Entry> { let std_path = warp_util::standardized_path::StandardizedPath::try_from_local(path).ok()?; if entry.path() == &std_path { diff --git a/crates/repo_metadata/src/local_model.rs b/crates/repo_metadata/src/local_model.rs index dee177014d..681aeb9c3c 100644 --- a/crates/repo_metadata/src/local_model.rs +++ b/crates/repo_metadata/src/local_model.rs @@ -435,15 +435,20 @@ impl LocalRepoMetadataModel { )); } - // Register this path with the watcher if we have one. + // Register this path with the watcher if we have one #[cfg(feature = "local_fs")] { if let Some(ref watcher) = self.watcher { let watch_path = local_path.clone(); + // Build the gitignore set (root + global) and interest list so + // the descend filter prunes gitignored subtrees while still + // watching registered ignored-path interests (e.g. skills). + let gitignores = crate::gitignores_for_directory(&watch_path); + let interests = self.ignored_path_interests.clone(); watcher.update(ctx, |watcher, _ctx| { std::mem::drop(watcher.register_path( &watch_path, - repo_watch_filter(), + repo_watch_filter(gitignores, interests), RecursiveMode::Recursive, )); }); diff --git a/crates/repo_metadata/src/watcher.rs b/crates/repo_metadata/src/watcher.rs index 3790ff2e45..b88dd8a08c 100644 --- a/crates/repo_metadata/src/watcher.rs +++ b/crates/repo_metadata/src/watcher.rs @@ -41,6 +41,12 @@ pub struct DirectoryWatcher { /// Handle to the internal processing queue model that orders scan & update tasks. processing_queue: ModelHandle, + + /// Component-sequence paths that should stay watched even when gitignored + /// (e.g. skill provider directories). Fed into the watch descend filter so + /// consumers keep getting updates for ignored subtrees they care about. + #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] + ignored_path_interests: Vec, } impl DirectoryWatcher { @@ -68,6 +74,7 @@ impl DirectoryWatcher { #[cfg(feature = "local_fs")] watcher: Some(fs_watcher), processing_queue, + ignored_path_interests: Vec::new(), } } @@ -92,6 +99,23 @@ impl DirectoryWatcher { #[cfg(feature = "local_fs")] watcher: Some(fs_watcher), processing_queue, + ignored_path_interests: Vec::new(), + } + } + + /// Registers component-sequence paths that should stay watched even when + /// gitignored. Mirrors `LocalRepoMetadataModel::register_ignored_path_interests` + /// but applies to the watcher backing `Repository` subscribers (LSP, MCP). + /// Must be called before repositories begin watching for the interest to + /// take effect on already-registered watches. + pub fn register_ignored_path_interests( + &mut self, + interests: impl IntoIterator, + ) { + for interest in interests { + if !self.ignored_path_interests.contains(&interest) { + self.ignored_path_interests.push(interest); + } } } @@ -321,6 +345,11 @@ impl DirectoryWatcher { let local_path = directory_path.to_local_path(); let registration_future = if let Some(ref watcher) = self.watcher { if let Some(local_path) = local_path.clone() { + // Build the gitignore set (root + global) and interest list up + // front so the descend filter prunes gitignored subtrees while + // still watching registered interests. + let gitignores = crate::gitignores_for_directory(&local_path); + let interests = self.ignored_path_interests.clone(); watcher.update(ctx, |watcher, _ctx| { use notify_debouncer_full::notify::RecursiveMode; @@ -328,7 +357,7 @@ impl DirectoryWatcher { Some(watcher.register_path( &local_path, - repo_watch_filter(), + repo_watch_filter(gitignores, interests), RecursiveMode::Recursive, )) }) From 10c4d2ec6daf5626f1bb75039e546bce00077b59 Mon Sep 17 00:00:00 2001 From: Yunfan Yang Date: Wed, 3 Jun 2026 11:26:32 -0400 Subject: [PATCH 2/3] self-review --- app/src/lib.rs | 25 ++++++++++++------------- crates/repo_metadata/src/repository.rs | 13 +++++++++---- crates/repo_metadata/src/watcher.rs | 17 ++++++++++++----- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index eb83ffb2dd..6789853e69 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1508,6 +1508,18 @@ pub(crate) fn initialize_app( #[cfg(not(target_family = "wasm"))] { ctx.add_singleton_model(DirectoryWatcher::new); + // Register the skill provider directories as ignored-path interests so + // the gitignore-pruning watch descend filter still watches gitignored + // skill directories (e.g. `.agents/skills`) for `Repository` + // subscribers (LSP, MCP). Registered before any repository begins + // watching so it gates descent on the very first registration. + DirectoryWatcher::handle(ctx).update(ctx, |watcher, _| { + watcher.register_ignored_path_interests( + ::ai::skills::SKILL_PROVIDER_DEFINITIONS + .iter() + .map(|provider| provider.skills_path.clone()), + ); + }); ctx.add_singleton_model(|_| DetectedRepositories::default()); if let Some(home_dir) = dirs::home_dir() { ctx.add_singleton_model(|ctx| HomeDirectoryWatcher::new(home_dir, ctx)); @@ -1563,19 +1575,6 @@ pub(crate) fn initialize_app( model }); - - // Mirror the skill interests onto the DirectoryWatcher so the - // gitignore-pruning descend filter still watches gitignored skill - // directories for `Repository` subscribers (LSP, MCP). Registered at - // startup, before any repository begins watching, so it gates descent - // on the very first registration. - DirectoryWatcher::handle(ctx).update(ctx, |watcher, _| { - watcher.register_ignored_path_interests( - ::ai::skills::SKILL_PROVIDER_DEFINITIONS - .iter() - .map(|provider| provider.skills_path.clone()), - ); - }); } { diff --git a/crates/repo_metadata/src/repository.rs b/crates/repo_metadata/src/repository.rs index 67427d7d92..52ab184813 100644 --- a/crates/repo_metadata/src/repository.rs +++ b/crates/repo_metadata/src/repository.rs @@ -328,10 +328,15 @@ impl Repository { let registration_future: BoxFuture<'static, Result<(), RepoMetadataError>> = if should_start_watching { let directories_to_watch = self.watch_paths(); - - Box::pin(DirectoryWatcher::handle(ctx).update(ctx, |watcher, ctx| { - watcher.start_watching_directories(directories_to_watch, ctx) - })) + // Reuse the gitignores we already built at construction so the + // watch descend filter doesn't re-read `.gitignore` from disk. + let gitignores = self.gitignores.clone(); + + Box::pin( + DirectoryWatcher::handle(ctx).update(ctx, move |watcher, ctx| { + watcher.start_watching_directories(directories_to_watch, gitignores, ctx) + }), + ) } else { Box::pin(ready(Ok(()))) }; diff --git a/crates/repo_metadata/src/watcher.rs b/crates/repo_metadata/src/watcher.rs index b88dd8a08c..9e8a875da8 100644 --- a/crates/repo_metadata/src/watcher.rs +++ b/crates/repo_metadata/src/watcher.rs @@ -15,6 +15,7 @@ use crate::{RepoMetadataError, Repository}; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { + use ignore::gitignore::Gitignore; use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent}; use crate::entry::{ extract_worktree_git_dir, is_commit_related_git_file, is_git_internal_path, @@ -108,6 +109,10 @@ impl DirectoryWatcher { /// but applies to the watcher backing `Repository` subscribers (LSP, MCP). /// Must be called before repositories begin watching for the interest to /// take effect on already-registered watches. + /// + /// Kept as a `Vec` (deduped on insert): registration happens a handful of + /// times at startup, while `start_watching_directory` reads it far more + /// often and benefits from a cheap clone. pub fn register_ignored_path_interests( &mut self, interests: impl IntoIterator, @@ -318,11 +323,12 @@ impl DirectoryWatcher { pub(crate) fn start_watching_directories( &mut self, directory_paths: Vec, + gitignores: Vec, ctx: &mut ModelContext, ) -> impl Future> { let futures: Vec<_> = directory_paths .into_iter() - .map(|path| self.start_watching_directory(&path, ctx)) + .map(|path| self.start_watching_directory(&path, gitignores.clone(), ctx)) .collect(); async move { @@ -340,15 +346,16 @@ impl DirectoryWatcher { pub(crate) fn start_watching_directory( &mut self, directory_path: &StandardizedPath, + gitignores: Vec, ctx: &mut ModelContext, ) -> impl Future> { let local_path = directory_path.to_local_path(); let registration_future = if let Some(ref watcher) = self.watcher { if let Some(local_path) = local_path.clone() { - // Build the gitignore set (root + global) and interest list up - // front so the descend filter prunes gitignored subtrees while - // still watching registered interests. - let gitignores = crate::gitignores_for_directory(&local_path); + // `gitignores` are the repo's cached root + global gitignores, + // threaded in from `Repository::start_watching` so we neither + // re-read `.gitignore` from disk nor re-enter the (already + // borrowed) `Repository` model here. let interests = self.ignored_path_interests.clone(); watcher.update(ctx, |watcher, _ctx| { use notify_debouncer_full::notify::RecursiveMode; From 8129e37aa458c1e6e96f39a6283a8bec2813d7cc Mon Sep 17 00:00:00 2001 From: Yunfan Yang Date: Wed, 3 Jun 2026 23:58:15 -0400 Subject: [PATCH 3/3] comments --- app/src/lib.rs | 6 +- crates/repo_metadata/src/entry.rs | 98 ++++++++----------- crates/repo_metadata/src/entry_tests.rs | 61 ++++++------ crates/repo_metadata/src/local_model.rs | 56 +++++------ crates/repo_metadata/src/local_model_tests.rs | 2 +- crates/repo_metadata/src/watcher.rs | 41 ++++---- crates/repo_metadata/src/wrapper_model.rs | 17 ++-- 7 files changed, 132 insertions(+), 149 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index 6789853e69..64729d3c9b 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1508,13 +1508,13 @@ pub(crate) fn initialize_app( #[cfg(not(target_family = "wasm"))] { ctx.add_singleton_model(DirectoryWatcher::new); - // Register the skill provider directories as ignored-path interests so + // Register the skill provider directories as force-included paths so // the gitignore-pruning watch descend filter still watches gitignored // skill directories (e.g. `.agents/skills`) for `Repository` // subscribers (LSP, MCP). Registered before any repository begins // watching so it gates descent on the very first registration. DirectoryWatcher::handle(ctx).update(ctx, |watcher, _| { - watcher.register_ignored_path_interests( + watcher.register_force_included_paths( ::ai::skills::SKILL_PROVIDER_DEFINITIONS .iter() .map(|provider| provider.skills_path.clone()), @@ -1545,7 +1545,7 @@ pub(crate) fn initialize_app( } else { RepoMetadataModel::new(ctx) }; - model.register_ignored_path_interests( + model.register_force_included_paths( ::ai::skills::SKILL_PROVIDER_DEFINITIONS .iter() .map(|provider| provider.skills_path.clone()), diff --git a/crates/repo_metadata/src/entry.rs b/crates/repo_metadata/src/entry.rs index 03cb4a4b17..2767678db3 100644 --- a/crates/repo_metadata/src/entry.rs +++ b/crates/repo_metadata/src/entry.rs @@ -73,7 +73,7 @@ pub(crate) struct BuildTreeOptions<'a> { pub max_depth: usize, pub current_depth: usize, pub ignored_path_strategy: &'a IgnoredPathStrategy, - pub ignored_path_interests: &'a [PathBuf], + pub force_included_paths: &'a [PathBuf], pub budget_exceeded_behavior: BudgetExceededBehavior, } @@ -128,7 +128,7 @@ impl Entry { ignored_path_strategy: &IgnoredPathStrategy, budget_exceeded_behavior: BudgetExceededBehavior, ) -> Result { - Self::build_tree_with_ignored_path_interests_and_ancestor( + Self::build_tree_with_force_included_paths_and_ancestor( path, files, gitignores, @@ -137,23 +137,24 @@ impl Entry { max_depth, current_depth, ignored_path_strategy, - ignored_path_interests: &[], + force_included_paths: &[], budget_exceeded_behavior, }, false, ) } - /// Builds a tree of entries from a given path, loading ignored paths that match - /// one of the supplied component-sequence interests instead of leaving them lazy. - pub(crate) fn build_tree_with_ignored_path_interests( + /// Builds a tree of entries from a given path, eagerly loading any path that + /// matches one of the supplied force-included paths instead of leaving it + /// lazy (see [`BuildTreeOptions::force_included_paths`]). + pub(crate) fn build_tree_with_force_included_paths( path: impl Into, files: &mut Vec, gitignores: &mut Vec, remaining_file_quota: Option<&mut usize>, options: BuildTreeOptions<'_>, ) -> Result { - Self::build_tree_with_ignored_path_interests_and_ancestor( + Self::build_tree_with_force_included_paths_and_ancestor( path, files, gitignores, @@ -174,7 +175,7 @@ impl Entry { ignored_path_strategy: &IgnoredPathStrategy, ancestor_is_ignored: bool, ) -> Result { - Self::build_tree_with_ignored_path_interests_and_ancestor( + Self::build_tree_with_force_included_paths_and_ancestor( path, files, gitignores, @@ -183,7 +184,7 @@ impl Entry { max_depth, current_depth, ignored_path_strategy, - ignored_path_interests: &[], + force_included_paths: &[], budget_exceeded_behavior: BudgetExceededBehavior::StopAndLazyLoad, }, ancestor_is_ignored, @@ -191,7 +192,7 @@ impl Entry { } #[allow(clippy::too_many_arguments)] - pub(crate) fn build_tree_with_ignored_path_interests_and_ancestor( + pub(crate) fn build_tree_with_force_included_paths_and_ancestor( path: impl Into, files: &mut Vec, gitignores: &mut Vec, @@ -257,18 +258,17 @@ impl Entry { // Budget handling. With `StopAndLazyLoad` (the default), once // the file quota is exhausted we stop expanding directories // and leave them as unloaded placeholders; directories on the - // path to a registered ignored-path interest (e.g. skill - // provider directories) are always expanded so - // discovery-critical files stay reachable. With `FailFast` we - // keep descending and abort below as soon as a file would - // exceed the budget. + // path to a force-included path (e.g. skill provider + // directories) are always expanded so discovery-critical + // files stay reachable. With `FailFast` we keep descending + // and abort below as soon as a file would exceed the budget. let should_expand = match options.budget_exceeded_behavior { BudgetExceededBehavior::FailFast => true, BudgetExceededBehavior::StopAndLazyLoad => { quota.is_none_or(|remaining| remaining > 0) - || matches_ignored_path_interest( + || matches_force_included_path( &job.path, - options.ignored_path_interests, + options.force_included_paths, ) } }; @@ -346,9 +346,9 @@ impl Entry { })); push_child(&mut nodes, job.index, child_index); // Lazy directories (past max depth, or ignored - // without a matching interest) stay unloaded. - // Everything else is queued for expansion, - // subject to the budget gate above. + // without a matching force-included path) stay + // unloaded. Everything else is queued for + // expansion, subject to the budget gate above. if !lazy { queue.push_back(DirJob { index: child_index, @@ -541,7 +541,7 @@ fn evaluate_entry( } } IgnoredPathStrategy::IncludeLazy => { - lazy = !matches_ignored_path_interest(curr_path, options.ignored_path_interests); + lazy = !matches_force_included_path(curr_path, options.force_included_paths); } IgnoredPathStrategy::Include => {} } @@ -629,7 +629,11 @@ pub fn is_git_internal_path(path: &Path) -> bool { }) } -fn matches_ignored_path_interest(path: &Path, ignored_path_interests: &[PathBuf]) -> bool { +/// Returns `true` when `path` is, contains, or lies on the way to one of the +/// `force_included_paths`. Each force-included path is a relative component +/// sequence (e.g. `.agents/skills`) matched against the tail of `path`, so a +/// match also holds for the ancestor prefixes leading to it. +fn matches_force_included_path(path: &Path, force_included_paths: &[PathBuf]) -> bool { let path_components: Vec<_> = path .components() .filter_map(|component| match component { @@ -641,8 +645,8 @@ fn matches_ignored_path_interest(path: &Path, ignored_path_interests: &[PathBuf] }) .collect(); - ignored_path_interests.iter().any(|interest| { - let interest_components: Vec<_> = interest + force_included_paths.iter().any(|force_included| { + let force_included_components: Vec<_> = force_included .components() .filter_map(|component| match component { Component::Normal(name) => Some(name), @@ -653,21 +657,21 @@ fn matches_ignored_path_interest(path: &Path, ignored_path_interests: &[PathBuf] }) .collect(); - if interest_components.is_empty() { + if force_included_components.is_empty() { return false; } if path_components - .windows(interest_components.len()) - .any(|window| window == interest_components.as_slice()) + .windows(force_included_components.len()) + .any(|window| window == force_included_components.as_slice()) { return true; } - (1..interest_components.len()).any(|prefix_len| { + (1..force_included_components.len()).any(|prefix_len| { path_components.len() >= prefix_len && path_components[path_components.len() - prefix_len..] - == interest_components[..prefix_len] + == force_included_components[..prefix_len] }) }) } @@ -911,38 +915,22 @@ fn descend_allowlist_matches(suffix: &[Component<'_>]) -> bool { } /// Returns whether a repository file watcher should descend into (and register -/// a watch on) `path`. +/// a watch on) the directory at `path`. /// -/// This is the descend predicate of [`repo_watch_filter`]. Precedence: -/// 1. `.git/` internals follow the existing allowlist -/// ([`should_watch_directory_in_git_path`]). -/// 2. Any directory on the path to — or inside — a registered ignored-path -/// interest is always watched, so consumers (e.g. skills) keep getting -/// updates for ignored subtrees they explicitly care about. This mirrors -/// the tree builder's lazy-load exception -/// ([`matches_ignored_path_interest`]) and, because that match also returns -/// `true` for the ancestor prefixes leading to an interest, it preserves -/// the watcher's monotonicity invariant. -/// 3. Otherwise, gitignored directories are pruned so we don't register -/// inotify watches on `node_modules`, build output, vendored deps, etc. -/// -/// Ancestor-aware matching (`check_ancestors = true`) is what makes pruning -/// safe w.r.t. the monotonicity invariant: a child of an ignored directory is -/// itself reported as ignored, so we never accept a descendant after -/// rejecting its parent. Directory-only re-include negations -/// (`parentdir/*` + `!parentdir/*/`) keep working because the negated -/// directory matches as not-ignored on its own path (last-match-wins via -/// `matched_path_or_any_parents`), so we still descend into it. +/// Directories inside `.git/` follow the watcher allowlist, force-included +/// paths are always watched even when gitignored, and any other gitignored +/// directory is pruned so we don't register watches on `node_modules`, build +/// output, vendored deps, etc. pub fn should_watch_repo_directory( path: &Path, gitignores: &[Gitignore], - ignored_path_interests: &[PathBuf], + force_included_paths: &[PathBuf], ) -> bool { if is_git_internal_path(path) { return should_watch_directory_in_git_path(path); } - if matches_ignored_path_interest(path, ignored_path_interests) { + if matches_force_included_path(path, force_included_paths) { return true; } @@ -965,7 +953,7 @@ pub fn should_watch_repo_directory( /// /// Descend predicate: see [`should_watch_repo_directory`]. In addition to the /// `.git/` allowlist, it prunes gitignored directories (honoring registered -/// ignored-path interests) so the recursive walk does not register watches on +/// force-included paths) so the recursive walk does not register watches on /// gitignored subtrees. /// /// `gitignores` should be the repo's root + global gitignores (as produced by @@ -977,10 +965,10 @@ pub fn should_watch_repo_directory( #[cfg(feature = "local_fs")] pub fn repo_watch_filter( gitignores: Vec, - ignored_path_interests: Vec, + force_included_paths: Vec, ) -> WatchFilter { let should_watch = - move |path: &Path| should_watch_repo_directory(path, &gitignores, &ignored_path_interests); + move |path: &Path| should_watch_repo_directory(path, &gitignores, &force_included_paths); WatchFilter::with_filter( Arc::new(should_watch), Arc::new(|path: &Path| !should_ignore_git_path(path)), diff --git a/crates/repo_metadata/src/entry_tests.rs b/crates/repo_metadata/src/entry_tests.rs index 0e553ed3ef..38d7757e09 100644 --- a/crates/repo_metadata/src/entry_tests.rs +++ b/crates/repo_metadata/src/entry_tests.rs @@ -212,69 +212,69 @@ fn should_watch_prunes_gitignored_directory() { } #[test] -fn should_watch_descends_to_interest_under_ignored_ancestor() { +fn should_watch_descends_to_force_included_under_ignored_ancestor() { let temp_dir = tempfile::tempdir().unwrap(); let root = dunce::canonicalize(temp_dir.path()).unwrap(); fs::create_dir_all(root.join(".agents/skills/test")).unwrap(); fs::create_dir(root.join(".agents/other")).unwrap(); let gitignores = vec![gitignore_rooted(&root, ".agents/\n")]; - let interests = vec![std::path::PathBuf::from(".agents/skills")]; + let force_included = vec![std::path::PathBuf::from(".agents/skills")]; // The whole `.agents` subtree is gitignored, but we still descend along the - // prefix to reach the registered interest, and into the interest subtree. + // prefix to reach the force-included path, and into its subtree. assert!(super::should_watch_repo_directory( &root.join(".agents"), &gitignores, - &interests + &force_included )); assert!(super::should_watch_repo_directory( &root.join(".agents/skills"), &gitignores, - &interests + &force_included )); assert!(super::should_watch_repo_directory( &root.join(".agents/skills/test"), &gitignores, - &interests + &force_included )); - // A sibling ignored dir that is not part of any interest is still pruned. + // A sibling ignored dir that is not force-included is still pruned. assert!(!super::should_watch_repo_directory( &root.join(".agents/other"), &gitignores, - &interests + &force_included )); } #[test] -fn should_watch_handles_nested_ignored_ancestor_with_deeper_interest() { +fn should_watch_handles_nested_ignored_ancestor_with_deeper_force_included() { let temp_dir = tempfile::tempdir().unwrap(); let root = dunce::canonicalize(temp_dir.path()).unwrap(); fs::create_dir_all(root.join("a/b/c")).unwrap(); fs::create_dir(root.join("a/b/other")).unwrap(); let gitignores = vec![gitignore_rooted(&root, "a/b/\n")]; - let interests = vec![std::path::PathBuf::from("a/b/c")]; + let force_included = vec![std::path::PathBuf::from("a/b/c")]; - // `a/b` is ignored but `a/b/c` is a registered interest: descend along the - // whole prefix and into the interest, while pruning the ignored sibling. + // `a/b` is ignored but `a/b/c` is force-included: descend along the whole + // prefix and into it, while pruning the ignored sibling. assert!(super::should_watch_repo_directory( &root.join("a"), &gitignores, - &interests + &force_included )); assert!(super::should_watch_repo_directory( &root.join("a/b"), &gitignores, - &interests + &force_included )); assert!(super::should_watch_repo_directory( &root.join("a/b/c"), &gitignores, - &interests + &force_included )); assert!(!super::should_watch_repo_directory( &root.join("a/b/other"), &gitignores, - &interests + &force_included )); } @@ -312,8 +312,9 @@ fn should_watch_descends_dir_only_reinclude_negation() { #[test] fn should_watch_preserves_git_internal_allowlist() { - // No gitignores / interests needed: `.git` handling short-circuits and is - // path-based, mirroring `should_watch_directory_in_git_path`. + // No gitignores / force-included paths needed: `.git` handling + // short-circuits and is path-based, mirroring + // `should_watch_directory_in_git_path`. let repo = std::path::Path::new("/home/user/project"); assert!(super::should_watch_repo_directory( &repo.join(".git/refs/heads"), @@ -346,7 +347,7 @@ fn build_skill_tree_with_gitignore(root: &std::path::Path, gitignore: &str) -> s let mut files = Vec::new(); let mut gitignores = Vec::new(); let mut file_limit = 1000; - super::Entry::build_tree_with_ignored_path_interests( + super::Entry::build_tree_with_force_included_paths( root, &mut files, &mut gitignores, @@ -355,7 +356,7 @@ fn build_skill_tree_with_gitignore(root: &std::path::Path, gitignore: &str) -> s max_depth: 200, current_depth: 0, ignored_path_strategy: &super::IgnoredPathStrategy::IncludeLazy, - ignored_path_interests: &[std::path::PathBuf::from(".agents/skills")], + force_included_paths: &[std::path::PathBuf::from(".agents/skills")], budget_exceeded_behavior: super::BudgetExceededBehavior::StopAndLazyLoad, }, ) @@ -437,7 +438,7 @@ fn ignored_agents_skills_directory_is_loaded_for_registered_provider_path() { } #[test] -fn unrelated_ignored_directory_stays_lazy_without_registered_interest() { +fn unrelated_ignored_directory_stays_lazy_without_registered_force_included() { virtual_fs::VirtualFS::test("unrelated_ignored_dir_lazy", |dirs, mut vfs| { vfs.mkdir("repo/.agents/skills/test") .mkdir("repo/target/debug") @@ -672,17 +673,17 @@ fn test_extract_worktree_git_dir() { ); } -/// Builds a tree with an explicit file budget and ignored-path interests using +/// Builds a tree with an explicit file budget and force-included paths using /// the lazy ignored-path strategy. fn build_with_budget( root: &std::path::Path, budget: usize, - interests: &[std::path::PathBuf], + force_included_paths: &[std::path::PathBuf], ) -> super::Entry { let mut files = Vec::new(); let mut gitignores = Vec::new(); let mut file_limit = budget; - super::Entry::build_tree_with_ignored_path_interests( + super::Entry::build_tree_with_force_included_paths( root, &mut files, &mut gitignores, @@ -691,7 +692,7 @@ fn build_with_budget( max_depth: 200, current_depth: 0, ignored_path_strategy: &super::IgnoredPathStrategy::IncludeLazy, - ignored_path_interests: interests, + force_included_paths, budget_exceeded_behavior: super::BudgetExceededBehavior::StopAndLazyLoad, }, ) @@ -743,7 +744,7 @@ fn build_tree_budget_covers_breadth_first_and_leaves_remainder_unloaded() { } #[test] -fn build_tree_budget_does_not_prune_interest_paths() { +fn build_tree_budget_does_not_prune_force_included_paths() { let temp_dir = tempfile::tempdir().unwrap(); let root = dunce::canonicalize(temp_dir.path()).unwrap(); @@ -757,13 +758,13 @@ fn build_tree_budget_does_not_prune_interest_paths() { fs::write(skill_dir.join("SKILL.md"), "name: test").unwrap(); // Tiny budget: the root expands and is immediately exhausted, but the - // registered interest path must still be loaded all the way down. - let interests = [std::path::PathBuf::from(".agents/skills")]; - let tree = build_with_budget(&root, 1, &interests); + // force-included path must still be loaded all the way down. + let force_included = [std::path::PathBuf::from(".agents/skills")]; + let tree = build_with_budget(&root, 1, &force_included); assert!( find_entry(&tree, &skill_dir.join("SKILL.md")).is_some(), - "interest-path files must load even when the budget is exhausted" + "force-included path files must load even when the budget is exhausted" ); } diff --git a/crates/repo_metadata/src/local_model.rs b/crates/repo_metadata/src/local_model.rs index 681aeb9c3c..2ebf4a6d9c 100644 --- a/crates/repo_metadata/src/local_model.rs +++ b/crates/repo_metadata/src/local_model.rs @@ -159,10 +159,11 @@ pub struct LocalRepoMetadataModel { /// events after applying watcher mutations. Only the remote server /// variant enables this. emit_incremental_updates: bool, - /// Component-sequence paths that consumers need loaded even when ignored. - /// For example, a consumer can register `.foo/bar` so ignored `.foo`, - /// `.foo/bar`, and descendants of `.foo/bar` are loaded into the tree. - ignored_path_interests: Vec, + /// Paths that must be loaded even when gitignored or beyond the tree's size + /// limit. For example, a consumer can register `.foo/bar` so ignored + /// `.foo`, `.foo/bar`, and descendants of `.foo/bar` are loaded into the + /// tree. + force_included_paths: Vec, } #[derive(Debug, Clone, Default)] @@ -247,7 +248,7 @@ impl LocalRepoMetadataModel { #[cfg(feature = "local_fs")] watcher: None, emit_incremental_updates: false, - ignored_path_interests: Vec::new(), + force_included_paths: Vec::new(), }; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { @@ -283,22 +284,20 @@ impl LocalRepoMetadataModel { self.emit_incremental_updates = enabled; } - /// Registers component-sequence paths that should be loaded even when ignored. + /// Registers paths that must be loaded even when gitignored or beyond the + /// tree's size limit. /// /// This stays intentionally generic: consumers own the meaning of the paths, /// while repo metadata only uses them to decide which ignored subtrees should /// be represented eagerly instead of as lazy placeholders. - pub fn register_ignored_path_interests( - &mut self, - interests: impl IntoIterator, - ) { - for interest in interests { + pub fn register_force_included_paths(&mut self, paths: impl IntoIterator) { + for path in paths { if !self - .ignored_path_interests + .force_included_paths .iter() - .any(|existing| existing == &interest) + .any(|existing| existing == &path) { - self.ignored_path_interests.push(interest); + self.force_included_paths.push(path); } } } @@ -354,14 +353,14 @@ impl LocalRepoMetadataModel { if let Some(IndexedRepoState::Indexed(state)) = self.repositories.get_mut(&repo_path) { let repo_path_clone = repo_path.clone(); let gitignores_clone = state.gitignores.clone(); - let ignored_path_interests = self.ignored_path_interests.clone(); + let force_included_paths = self.force_included_paths.clone(); let lazy_load = self.lazy_loaded_paths.contains_key(&repo_path); ctx.spawn( async move { let mutations = Self::compute_file_tree_mutations( &repo_scoped_update, &gitignores_clone, - &ignored_path_interests, + &force_included_paths, ) .await; (mutations, repo_path_clone, lazy_load) @@ -440,15 +439,16 @@ impl LocalRepoMetadataModel { { if let Some(ref watcher) = self.watcher { let watch_path = local_path.clone(); - // Build the gitignore set (root + global) and interest list so - // the descend filter prunes gitignored subtrees while still - // watching registered ignored-path interests (e.g. skills). + // Build the gitignore set (root + global) and force-included + // path list so the descend filter prunes gitignored subtrees + // while still watching registered force-included paths (e.g. + // skills). let gitignores = crate::gitignores_for_directory(&watch_path); - let interests = self.ignored_path_interests.clone(); + let force_included_paths = self.force_included_paths.clone(); watcher.update(ctx, |watcher, _ctx| { std::mem::drop(watcher.register_path( &watch_path, - repo_watch_filter(gitignores, interests), + repo_watch_filter(gitignores, force_included_paths), RecursiveMode::Recursive, )); }); @@ -640,7 +640,7 @@ impl LocalRepoMetadataModel { async fn compute_file_tree_mutations( update: &RepoUpdate, gitignores: &[Gitignore], - ignored_path_interests: &[PathBuf], + force_included_paths: &[PathBuf], ) -> Vec { let mut mutations = Vec::new(); @@ -661,7 +661,7 @@ impl LocalRepoMetadataModel { let mut files = Vec::new(); let mut gitignores = gitignores.to_owned(); let mut file_limit = MAX_FILES_PER_REPO; - match Entry::build_tree_with_ignored_path_interests_and_ancestor( + match Entry::build_tree_with_force_included_paths_and_ancestor( path_to_add, &mut files, &mut gitignores, @@ -670,7 +670,7 @@ impl LocalRepoMetadataModel { max_depth: MAX_TREE_DEPTH, current_depth: 0, ignored_path_strategy: &IgnoredPathStrategy::IncludeLazy, - ignored_path_interests, + force_included_paths, budget_exceeded_behavior: BudgetExceededBehavior::StopAndLazyLoad, }, is_ignored, @@ -953,7 +953,7 @@ impl LocalRepoMetadataModel { // Build the complete file tree for the repository asynchronously let repo_path_for_build = local_path; let gitignores_for_build = gitignores.clone(); - let ignored_path_interests = self.ignored_path_interests.clone(); + let force_included_paths = self.force_included_paths.clone(); let repo_path_str_for_log = std_path.to_string(); let std_path_for_completion = std_path; let repository_handle_for_completion = repository_handle.clone(); @@ -967,11 +967,11 @@ impl LocalRepoMetadataModel { // stops descending breadth-first and leaves the remaining // directories as unloaded placeholders (lazy-loaded on demand) // instead of failing the whole build. Gitignored subtrees stay - // lazy and registered ignored-path interests are always loaded; + // lazy and registered force-included paths are always loaded; // both are handled inside the builder. let mut file_limit = MAX_FILES_PER_REPO; - let build_result = Entry::build_tree_with_ignored_path_interests( + let build_result = Entry::build_tree_with_force_included_paths( &repo_path_for_build, &mut files, &mut gitignores_for_build, @@ -980,7 +980,7 @@ impl LocalRepoMetadataModel { max_depth: MAX_TREE_DEPTH, current_depth: 0, ignored_path_strategy: &IgnoredPathStrategy::IncludeLazy, - ignored_path_interests: &ignored_path_interests, + force_included_paths: &force_included_paths, budget_exceeded_behavior: BudgetExceededBehavior::StopAndLazyLoad, }, ); diff --git a/crates/repo_metadata/src/local_model_tests.rs b/crates/repo_metadata/src/local_model_tests.rs index 005d41ed4f..58f5b69711 100644 --- a/crates/repo_metadata/src/local_model_tests.rs +++ b/crates/repo_metadata/src/local_model_tests.rs @@ -32,7 +32,7 @@ impl LocalRepoMetadataModel { #[cfg(feature = "local_fs")] watcher: Default::default(), emit_incremental_updates: false, - ignored_path_interests: Vec::new(), + force_included_paths: Vec::new(), } } } diff --git a/crates/repo_metadata/src/watcher.rs b/crates/repo_metadata/src/watcher.rs index 9e8a875da8..324a7444cc 100644 --- a/crates/repo_metadata/src/watcher.rs +++ b/crates/repo_metadata/src/watcher.rs @@ -43,11 +43,11 @@ pub struct DirectoryWatcher { /// Handle to the internal processing queue model that orders scan & update tasks. processing_queue: ModelHandle, - /// Component-sequence paths that should stay watched even when gitignored - /// (e.g. skill provider directories). Fed into the watch descend filter so - /// consumers keep getting updates for ignored subtrees they care about. + /// Paths that must be watched (and indexed) even when they are gitignored + /// or beyond the tree's size limit — e.g. skill provider directories that + /// consumers (LSP, MCP) need live updates for. #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] - ignored_path_interests: Vec, + force_included_paths: Vec, } impl DirectoryWatcher { @@ -75,7 +75,7 @@ impl DirectoryWatcher { #[cfg(feature = "local_fs")] watcher: Some(fs_watcher), processing_queue, - ignored_path_interests: Vec::new(), + force_included_paths: Vec::new(), } } @@ -100,26 +100,19 @@ impl DirectoryWatcher { #[cfg(feature = "local_fs")] watcher: Some(fs_watcher), processing_queue, - ignored_path_interests: Vec::new(), + force_included_paths: Vec::new(), } } - /// Registers component-sequence paths that should stay watched even when - /// gitignored. Mirrors `LocalRepoMetadataModel::register_ignored_path_interests` - /// but applies to the watcher backing `Repository` subscribers (LSP, MCP). - /// Must be called before repositories begin watching for the interest to - /// take effect on already-registered watches. - /// - /// Kept as a `Vec` (deduped on insert): registration happens a handful of - /// times at startup, while `start_watching_directory` reads it far more - /// often and benefits from a cheap clone. - pub fn register_ignored_path_interests( - &mut self, - interests: impl IntoIterator, - ) { - for interest in interests { - if !self.ignored_path_interests.contains(&interest) { - self.ignored_path_interests.push(interest); + /// Registers paths that must be watched even when gitignored. Mirrors + /// `LocalRepoMetadataModel::register_force_included_paths` but applies to + /// the watcher backing `Repository` subscribers (LSP, MCP). Must be called + /// before repositories begin watching to take effect on already-registered + /// watches. + pub fn register_force_included_paths(&mut self, paths: impl IntoIterator) { + for path in paths { + if !self.force_included_paths.contains(&path) { + self.force_included_paths.push(path); } } } @@ -356,7 +349,7 @@ impl DirectoryWatcher { // threaded in from `Repository::start_watching` so we neither // re-read `.gitignore` from disk nor re-enter the (already // borrowed) `Repository` model here. - let interests = self.ignored_path_interests.clone(); + let force_included_paths = self.force_included_paths.clone(); watcher.update(ctx, |watcher, _ctx| { use notify_debouncer_full::notify::RecursiveMode; @@ -364,7 +357,7 @@ impl DirectoryWatcher { Some(watcher.register_path( &local_path, - repo_watch_filter(gitignores, interests), + repo_watch_filter(gitignores, force_included_paths), RecursiveMode::Recursive, )) }) diff --git a/crates/repo_metadata/src/wrapper_model.rs b/crates/repo_metadata/src/wrapper_model.rs index 39a65fb74f..e210ca402b 100644 --- a/crates/repo_metadata/src/wrapper_model.rs +++ b/crates/repo_metadata/src/wrapper_model.rs @@ -305,19 +305,20 @@ impl RepoMetadataModel { }) } - /// Registers component-sequence paths that should be loaded even when ignored. + /// Registers paths that must be loaded even when gitignored or beyond the + /// tree's size limit. /// - /// This delegates to the local model because ignored-path matching happens - /// while building local file trees. Remote repositories receive the resulting - /// file-tree metadata over the existing remote sync protocol. - pub fn register_ignored_path_interests( + /// This delegates to the local model because force-included path matching + /// happens while building local file trees. Remote repositories receive the + /// resulting file-tree metadata over the existing remote sync protocol. + pub fn register_force_included_paths( &self, - interests: impl IntoIterator, + paths: impl IntoIterator, ctx: &mut ModelContext, ) { - let interests: Vec<_> = interests.into_iter().collect(); + let paths: Vec<_> = paths.into_iter().collect(); self.local.update(ctx, |local, _| { - local.register_ignored_path_interests(interests); + local.register_force_included_paths(paths); }); }