diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index ce2f34bc78d52d..0ee6f0cb310ed1 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3340,12 +3340,16 @@ async fn is_dot_git(path: &Path, fs: &dyn Fs) -> bool { } async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { + let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); + build_gitignore_with_root(abs_path, parent, fs).await +} + +async fn build_gitignore_with_root(abs_path: &Path, root: &Path, fs: &dyn Fs) -> Result { let contents = fs .load(abs_path) .await .with_context(|| format!("failed to load gitignore file at {}", abs_path.display()))?; - let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); - let mut builder = GitignoreBuilder::new(parent); + let mut builder = GitignoreBuilder::new(root); for line in contents.lines() { builder.add_line(Some(abs_path.into()), line)?; } @@ -5329,7 +5333,9 @@ impl BackgroundScanner { // Load gitignores asynchronously (outside the lock) let mut loaded_excludes: Vec<(Arc, Arc)> = Vec::new(); for (work_dir_abs_path, exclude_abs_path) in excludes_to_load { - if let Ok(current_exclude) = build_gitignore(&exclude_abs_path, self.fs.as_ref()).await + if let Ok(current_exclude) = + build_gitignore_with_root(&exclude_abs_path, &work_dir_abs_path, self.fs.as_ref()) + .await { loaded_excludes.push((work_dir_abs_path, Arc::new(current_exclude))); } @@ -5641,7 +5647,9 @@ async fn discover_ancestor_git_repo( let (_, common_dir_abs_path) = discover_git_paths(&dot_git_abs_path, fs.as_ref()).await; let repo_exclude_abs_path = common_dir_abs_path.join(REPO_EXCLUDE); - if let Ok(repo_exclude) = build_gitignore(&repo_exclude_abs_path, fs.as_ref()).await { + if let Ok(repo_exclude) = + build_gitignore_with_root(&repo_exclude_abs_path, ancestor, fs.as_ref()).await + { exclude = Some(Arc::new(repo_exclude)); } diff --git a/crates/worktree/tests/integration/worktree_tests.rs b/crates/worktree/tests/integration/worktree_tests.rs index 2ae248ad0e4053..7691a68392c8f7 100644 --- a/crates/worktree/tests/integration/worktree_tests.rs +++ b/crates/worktree/tests/integration/worktree_tests.rs @@ -3017,6 +3017,69 @@ async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext }); } +#[gpui::test] +async fn test_repo_exclude_anchored_pattern(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor); + let project_dir = Path::new(path!("/project")); + fs.insert_tree( + project_dir, + json!({ + ".git": { + "info": { + "exclude": "vendor/cache" + } + }, + "vendor": { + "cache": { + "blob.bin": "", + }, + "keep.txt": "", + }, + "elsewhere": { + "vendor": { + "cache": { + "blob.bin": "", + }, + }, + }, + }), + ) + .await; + + let worktree = Worktree::local( + project_dir, + true, + fs.clone(), + Default::default(), + true, + WorktreeId::from_proto(0), + &mut cx.to_async(), + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + // An anchored pattern (containing a `/`) is matched relative to the work + // tree root, so only the top-level `vendor/cache` is ignored. + worktree.update(cx, |worktree, _cx| { + check_worktree_entries( + worktree, + WorktreeExpectations { + ignored_paths: &["vendor/cache"], + tracked_paths: &["vendor/keep.txt", "elsewhere/vendor/cache"], + ..Default::default() + }, + ); + }); +} + #[derive(Default)] struct WorktreeExpectations { excluded_paths: &'static [&'static str],