From 2257e5260bfcbb1a8372fd14cca492523a24a8bc Mon Sep 17 00:00:00 2001 From: AlphaCat Date: Thu, 17 Sep 2026 21:23:34 +0800 Subject: [PATCH] fix(fs): scope ignore rules to the workspace's own git repository The file views walker (fs_list/fs_glob/fs_grep/fs_mention_list) applied gitignore rules even when the workspace is not a git repository (require_git(false)) and also honored ignore files in ancestor directories outside the workspace root (WalkBuilder's default parents(true)). Entries matching such rules silently vanished from the file tree - e.g. digit-leading folders matched by a stray [0-9]* rule, or folders created in Explorer that matched an ancestor's gitignore. - parents(false): the workspace root is the semantic boundary; outside ignore rules no longer leak in. - require_git(true): gitignore/global/exclude filtering only applies inside an actual git repository. - Existing gitignore-dependent test fixtures now initialize a fake .git directory; two regression tests cover the non-repo and ancestor-leak cases. Fixes #820 --- .../src-tauri/src/commands/workspace/fs.rs | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs b/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs index 963054103..89e10b9c8 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/fs.rs @@ -3904,11 +3904,18 @@ fn build_workspace_walker( let mut builder = WalkBuilder::new(base); builder .hidden(!visibility.include_system_hidden) + // The workspace root is the semantic boundary of the file views: + // ignore rules living in ancestor directories (monorepo parents, an + // unrelated ~/.gitignore chain) must not filter entries here (#820). + .parents(false) .ignore(!visibility.include_ignored) .git_ignore(!visibility.include_ignored) .git_global(!visibility.include_ignored) .git_exclude(!visibility.include_ignored) - .require_git(false) + // gitignore semantics only exist inside an actual git repository. A + // plain folder that happens to contain a .gitignore must not have + // entries silently dropped from the file tree (#820). + .require_git(true) .follow_links(false); let filter_macos_hidden = cfg!(target_os = "macos") && !visibility.include_system_hidden; if filter_macos_hidden || skip_common_dirs { @@ -4752,6 +4759,12 @@ mod tests { std::env::temp_dir().join(format!("liveagent-{name}-{suffix}")) } + // require_git(true): fixtures relying on gitignore filtering must look + // like a git repository (an empty .git directory is enough). + fn init_fake_git_repo(workdir: &Path) { + fs::create_dir_all(workdir.join(".git")).expect("create fake .git"); + } + fn list_test_entries(workdir: &Path, show_hidden: Option) -> Vec { fs_list_sync( workdir.display().to_string(), @@ -5727,6 +5740,7 @@ mod tests { #[test] fn list_respects_gitignore_and_rejects_outside_paths() { let workdir = unique_test_workdir("list-ignore"); + init_fake_git_repo(&workdir); fs::create_dir_all(workdir.join("src")).expect("create src"); fs::create_dir_all(workdir.join("ignored_dir")).expect("create ignored dir"); fs::write( @@ -5781,9 +5795,55 @@ mod tests { let _ = fs::remove_dir_all(workdir); } + // #820 regression: a .gitignore inside a plain (non-git) workspace must + // not hide entries — digit-leading folders vanished for users whose + // stray ignore rules matched patterns like `[0-9]*`. + #[test] + fn list_ignores_gitignore_outside_git_repository() { + let workdir = unique_test_workdir("list-nongit-gitignore"); + fs::create_dir_all(workdir.join("2024-01")).expect("create digit dir"); + fs::create_dir_all(workdir.join("alpha")).expect("create alpha"); + fs::write(workdir.join(".gitignore"), "[0-9]*\n").expect("write gitignore"); + + let paths: Vec = list_test_entries(&workdir, Some(false)) + .into_iter() + .map(|entry| entry.path) + .collect(); + assert!( + paths.contains(&"2024-01".to_string()), + "non-repo .gitignore must not hide entries: {paths:?}" + ); + assert!(paths.contains(&"alpha".to_string())); + + let _ = fs::remove_dir_all(workdir); + } + + // #820 regression: ignore rules living in ancestor directories outside + // the workspace root must not leak into the workspace listing. + #[test] + fn list_ignores_ancestor_gitignore_outside_workspace_root() { + let parent = unique_test_workdir("list-parent-gitignore"); + let workdir = parent.join("sub"); + fs::create_dir_all(workdir.join("10-notes")).expect("create digit dir"); + init_fake_git_repo(&parent); + fs::write(parent.join(".gitignore"), "[0-9]*\n").expect("write parent gitignore"); + + let paths: Vec = list_test_entries(&workdir, Some(false)) + .into_iter() + .map(|entry| entry.path) + .collect(); + assert!( + paths.contains(&"10-notes".to_string()), + "ancestor .gitignore must not filter the workspace listing: {paths:?}" + ); + + let _ = fs::remove_dir_all(parent); + } + #[test] fn list_hidden_toggle_includes_and_marks_ignored_and_dot_entries() { let workdir = unique_test_workdir("list-hidden-toggle"); + init_fake_git_repo(&workdir); fs::create_dir_all(workdir.join(".hidden_dir")).expect("create hidden dir"); fs::create_dir_all(workdir.join("ignored_dir")).expect("create ignored dir"); fs::write(workdir.join(".gitignore"), "ignored_dir/\nignored.txt\n") @@ -5944,8 +6004,9 @@ mod tests { } #[test] - fn mention_list_respects_gitignore_without_git_repository() { + fn mention_list_respects_gitignore_inside_git_repository() { let workdir = unique_test_workdir("mention-gitignore"); + init_fake_git_repo(&workdir); fs::create_dir_all(workdir.join("src")).expect("create src"); fs::create_dir_all(workdir.join("ignored_dir")).expect("create ignored dir"); fs::write( @@ -5979,6 +6040,7 @@ mod tests { #[test] fn mention_list_hidden_toggle_includes_and_marks_filtered_entries() { let workdir = unique_test_workdir("mention-hidden-toggle"); + init_fake_git_repo(&workdir); fs::create_dir_all(workdir.join("node_modules/pkg")).expect("create node_modules"); fs::create_dir_all(workdir.join("ignored_dir")).expect("create ignored dir"); fs::write(workdir.join(".gitignore"), "ignored_dir/\n").expect("write gitignore");