search: Fix ignored file search in nested Git directories (#60772)
# Objective - Fix ignored file search in nested Git directories (even just empty `.git` directories, not real git repositories) - Fixes #52328 ## Solution Keep parent ignore rules when rebuilding a nested repository's ignore stack. This hides root-ignored files again after excluded-file search is turned off, without breaking global ignore handling. For example: ```ascii project/ ├── .git/ ├── .gitignore # log/ ├── app/ │ └── a.txt # hello └── log/ ├── .git/ # nested repository marker └── b.txt # hello ``` Both `a.txt` and `b.txt` have string `hello`. Initially, searching for `hello` returns only `a.txt`. Enabling `Also search files ignored by configuration` rescans `log/` and returns both files. Before this change, disabling the option still returned both files because the rescan dropped the parent `.gitignore` and marked `b.txt` as included. ## Testing - Did you test these changes? If so, how? To test changes in this pull request create a simple project with the following dir structure: ``` .git .gitignore a/a.txt b/b.txt b/.git # this is an empty directory ``` Add `b` folder as an ignore entry to the `.gitignore` file: `b/` - Are there any parts that need more testing? No - How can other people (reviewers) test your changes? Is there anything specific they need to know? Use the test scenario above - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? macOS ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase By using the structure above here is how it works before changes here: https://github.com/user-attachments/assets/2028123e-388e-4390-b131-ddf4642ab5f3 Notice that `hello` string from file `b.txt` is listed in search results regardless of the option `Include excluded files` And here is after: https://github.com/user-attachments/assets/99c627a2-e16a-4fdd-979b-2e0bacbd827d Notice that `hello` string from file `b.txt` is **not** listed in search results when the option `Include excluded files` is disabled. Release Notes: - Fixed ignored file search in nested Git directories
This commit is contained in:
1 parent
cc6d83e1d4
commit
bfc278bee0
3 files changed
+183
-10
No files matched your search
@@ -2823,6 +2823,175 @@ pub mod tests {
|
||||
assert_eq!(split_glob_patterns(r"a\\\,b"), vec![r"a\\\,b"]);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
#[gpui::test]
|
||||
async fn test_ignored_dot_git_directory_results_follow_include_ignored_option(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
".gitignore": "log/\n",
|
||||
"app": {
|
||||
"a.txt": "hello",
|
||||
},
|
||||
"log": {
|
||||
".git": {},
|
||||
"b.txt": "hello",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
let search = cx.new(|cx| ProjectSearch::new(project, cx));
|
||||
let search_view = cx.add_window(|window, cx| {
|
||||
ProjectSearchView::new(workspace.downgrade(), search, window, cx, None)
|
||||
});
|
||||
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
})
|
||||
.unwrap();
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
})
|
||||
.unwrap();
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
#[gpui::test]
|
||||
async fn test_unignored_dot_git_directory_results_are_included(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"app": {
|
||||
"a.txt": "hello",
|
||||
},
|
||||
"log": {
|
||||
".git": {},
|
||||
"b.txt": "hello",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
let search = cx.new(|cx| ProjectSearch::new(project, cx));
|
||||
let search_view = cx.add_window(|window, cx| {
|
||||
ProjectSearchView::new(workspace.downgrade(), search, window, cx, None)
|
||||
});
|
||||
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
#[gpui::test]
|
||||
async fn test_nested_gitignore_results_follow_include_ignored_option(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"app": {
|
||||
"a.txt": "hello",
|
||||
},
|
||||
"log": {
|
||||
".git": {},
|
||||
".gitignore": "b.txt\n",
|
||||
"b.txt": "hello",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
let search = cx.new(|cx| ProjectSearch::new(project, cx));
|
||||
let search_view = cx.add_window(|window, cx| {
|
||||
ProjectSearchView::new(workspace.downgrade(), search, window, cx, None)
|
||||
});
|
||||
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
})
|
||||
.unwrap();
|
||||
perform_search(search_view, "hello", cx);
|
||||
assert_eq!(
|
||||
search_view
|
||||
.update(cx, |search_view, _, cx| {
|
||||
search_view.entity.read(cx).match_ranges.len()
|
||||
})
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
#[gpui::test]
|
||||
async fn test_project_search(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -85,12 +85,12 @@ impl IgnoreStack {
|
||||
IgnoreStackEntry::Global { ignore } => {
|
||||
let combined_path;
|
||||
let abs_path = if let Some(repo_root) = self.repo_root.as_ref() {
|
||||
combined_path = ignore.path().join(
|
||||
if let Ok(relative_path) = abs_path.strip_prefix(repo_root) {
|
||||
combined_path = ignore.path().join(relative_path);
|
||||
&combined_path
|
||||
} else {
|
||||
abs_path
|
||||
.strip_prefix(repo_root)
|
||||
.expect("repo root should be a parent of matched path"),
|
||||
);
|
||||
&combined_path
|
||||
}
|
||||
} else {
|
||||
abs_path
|
||||
};
|
||||
|
||||
@@ -2970,10 +2970,11 @@ impl LocalSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten();
|
||||
if metadata.is_some() {
|
||||
repo_root = Some(Arc::from(ancestor));
|
||||
break;
|
||||
if repo_root.is_none() {
|
||||
let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten();
|
||||
if metadata.is_some() {
|
||||
repo_root = Some(Arc::from(ancestor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2990,11 +2991,14 @@ impl LocalSnapshot {
|
||||
ignore_stack = ignore_stack.append(IgnoreKind::RepoExclude, repo_exclude.clone());
|
||||
}
|
||||
ignore_stack.repo_root = repo_root;
|
||||
let mut ancestor_ignore_stack = ignore_stack.clone();
|
||||
for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
|
||||
if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
|
||||
if ancestor_ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
|
||||
ignore_stack = IgnoreStack::all();
|
||||
break;
|
||||
} else if let Some(ignore) = ignore {
|
||||
let kind = IgnoreKind::Gitignore(parent_abs_path.into());
|
||||
ancestor_ignore_stack = ancestor_ignore_stack.append(kind, ignore.clone());
|
||||
ignore_stack =
|
||||
ignore_stack.append(IgnoreKind::Gitignore(parent_abs_path.into()), ignore);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user