Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions crates/agent-gui/src-tauri/src/commands/workspace/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<bool>) -> Vec<ListEntry> {
fs_list_sync(
workdir.display().to_string(),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<String> = 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<String> = 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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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");
Expand Down
Loading