Merge pull request #1309 from zed-industries/gitignore-traverse-ancestors
Honor `.gitignore` files above worktree's root
This commit is contained in:
@@ -4,7 +4,7 @@ use gpui::{
|
||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
path::Path,
|
||||
@@ -134,17 +134,40 @@ impl FileFinder {
|
||||
}
|
||||
|
||||
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let search_id = util::post_inc(&mut self.search_count);
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
let project = self.project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
|
||||
})
|
||||
.await;
|
||||
let matches = fuzzy::match_paths(
|
||||
candidate_sets.as_slice(),
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
cx.background(),
|
||||
)
|
||||
.await;
|
||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_matches(search_id, did_cancel, query, matches, cx)
|
||||
@@ -389,6 +412,51 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ignored_files(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/ancestor",
|
||||
json!({
|
||||
".gitignore": "ignored-root",
|
||||
"ignored-root": {
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
},
|
||||
"tracked-root": {
|
||||
".gitignore": "height",
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
app_state.fs.clone(),
|
||||
[
|
||||
"/ancestor/tracked-root".as_ref(),
|
||||
"/ancestor/ignored-root".as_ref(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
|
||||
.await;
|
||||
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
@@ -475,4 +543,34 @@ mod tests {
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"dir3": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let finder = finder.read(cx);
|
||||
assert_eq!(finder.matches.len(), 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc};
|
||||
pub enum IgnoreStack {
|
||||
None,
|
||||
Some {
|
||||
base: Arc<Path>,
|
||||
abs_base_path: Arc<Path>,
|
||||
ignore: Arc<Gitignore>,
|
||||
parent: Arc<IgnoreStack>,
|
||||
},
|
||||
@@ -24,19 +24,19 @@ impl IgnoreStack {
|
||||
matches!(self, IgnoreStack::All)
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>, base: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||
match self.as_ref() {
|
||||
IgnoreStack::All => self,
|
||||
_ => Arc::new(Self::Some {
|
||||
base,
|
||||
abs_base_path,
|
||||
ignore,
|
||||
parent: self,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool {
|
||||
if is_dir && path.file_name() == Some(OsStr::new(".git")) {
|
||||
pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
|
||||
if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ impl IgnoreStack {
|
||||
Self::None => false,
|
||||
Self::All => true,
|
||||
Self::Some {
|
||||
base,
|
||||
abs_base_path,
|
||||
ignore,
|
||||
parent: prev,
|
||||
} => match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) {
|
||||
ignore::Match::None => prev.is_path_ignored(path, is_dir),
|
||||
} => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
|
||||
ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
|
||||
ignore::Match::Ignore(_) => true,
|
||||
ignore::Match::Whitelist(_) => false,
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||
@@ -58,7 +57,7 @@ use std::{
|
||||
rc::Rc,
|
||||
str,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
@@ -5678,43 +5677,6 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn match_paths<'a>(
|
||||
&self,
|
||||
query: &'a str,
|
||||
include_ignored: bool,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &'a AtomicBool,
|
||||
cx: &AppContext,
|
||||
) -> impl 'a + Future<Output = Vec<PathMatch>> {
|
||||
let worktrees = self
|
||||
.worktrees(cx)
|
||||
.filter(|worktree| worktree.read(cx).is_visible())
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| CandidateSet {
|
||||
snapshot: worktree.read(cx).snapshot(),
|
||||
include_ignored,
|
||||
include_root_name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let background = cx.background().clone();
|
||||
async move {
|
||||
fuzzy::match_paths(
|
||||
candidate_sets.as_slice(),
|
||||
query,
|
||||
smart_case,
|
||||
max_results,
|
||||
cancel_flag,
|
||||
background,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn edits_from_lsp(
|
||||
&mut self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
@@ -5942,14 +5904,14 @@ impl OpenBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
struct CandidateSet {
|
||||
snapshot: Snapshot,
|
||||
include_ignored: bool,
|
||||
include_root_name: bool,
|
||||
pub struct PathMatchCandidateSet {
|
||||
pub snapshot: Snapshot,
|
||||
pub include_ignored: bool,
|
||||
pub include_root_name: bool,
|
||||
}
|
||||
|
||||
impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
|
||||
type Candidates = CandidateSetIter<'a>;
|
||||
impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
|
||||
type Candidates = PathMatchCandidateSetIter<'a>;
|
||||
|
||||
fn id(&self) -> usize {
|
||||
self.snapshot.id().to_usize()
|
||||
@@ -5974,23 +5936,23 @@ impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
|
||||
}
|
||||
|
||||
fn candidates(&'a self, start: usize) -> Self::Candidates {
|
||||
CandidateSetIter {
|
||||
PathMatchCandidateSetIter {
|
||||
traversal: self.snapshot.files(self.include_ignored, start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CandidateSetIter<'a> {
|
||||
pub struct PathMatchCandidateSetIter<'a> {
|
||||
traversal: Traversal<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for CandidateSetIter<'a> {
|
||||
type Item = PathMatchCandidate<'a>;
|
||||
impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
|
||||
type Item = fuzzy::PathMatchCandidate<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.traversal.next().map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
PathMatchCandidate {
|
||||
fuzzy::PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ use language::{
|
||||
};
|
||||
use lsp::Url;
|
||||
use serde_json::json;
|
||||
use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
|
||||
use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
|
||||
use unindent::Unindent as _;
|
||||
use util::{assert_set_eq, test::temp_tree};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"apple": "",
|
||||
@@ -38,7 +38,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(tree.file_count(), 5);
|
||||
@@ -47,23 +46,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
|
||||
tree.inode_for_path("finnochio/grape")
|
||||
);
|
||||
});
|
||||
|
||||
let cancel_flag = Default::default();
|
||||
let results = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.match_paths("bna", false, false, 10, &cancel_flag, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|result| result.path)
|
||||
.collect::<Vec<Arc<Path>>>(),
|
||||
vec![
|
||||
PathBuf::from("banana/carrot/date").into(),
|
||||
PathBuf::from("banana/carrot/endive").into(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1645,28 +1627,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
|
||||
chunks
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"dir3": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
|
||||
let cancel_flag = Default::default();
|
||||
let results = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.match_paths("dir", false, false, 10, &cancel_flag, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_definition(cx: &mut gpui::TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
|
||||
+153
-78
@@ -102,7 +102,7 @@ pub struct Snapshot {
|
||||
#[derive(Clone)]
|
||||
pub struct LocalSnapshot {
|
||||
abs_path: Arc<Path>,
|
||||
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
@@ -370,7 +370,7 @@ impl LocalWorktree {
|
||||
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
||||
let mut snapshot = LocalSnapshot {
|
||||
abs_path,
|
||||
ignores: Default::default(),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
removed_entry_ids: Default::default(),
|
||||
next_entry_id,
|
||||
snapshot: Snapshot {
|
||||
@@ -819,8 +819,8 @@ impl LocalWorktree {
|
||||
{
|
||||
let mut snapshot = this.background_snapshot.lock();
|
||||
entry.is_ignored = snapshot
|
||||
.ignore_stack_for_path(&path, entry.is_dir())
|
||||
.is_path_ignored(&path, entry.is_dir());
|
||||
.ignore_stack_for_abs_path(&abs_path, entry.is_dir())
|
||||
.is_abs_path_ignored(&abs_path, entry.is_dir());
|
||||
if let Some(old_path) = old_path {
|
||||
snapshot.remove_path(&old_path);
|
||||
}
|
||||
@@ -1331,11 +1331,12 @@ impl LocalSnapshot {
|
||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
let abs_path = self.abs_path.join(&entry.path);
|
||||
match build_gitignore(&abs_path, fs) {
|
||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||
Ok(ignore) => {
|
||||
let ignore_dir_path = entry.path.parent().unwrap();
|
||||
self.ignores
|
||||
.insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id));
|
||||
self.ignores_by_parent_abs_path.insert(
|
||||
abs_path.parent().unwrap().into(),
|
||||
(Arc::new(ignore), self.scan_id),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
@@ -1387,7 +1388,10 @@ impl LocalSnapshot {
|
||||
};
|
||||
|
||||
if let Some(ignore) = ignore {
|
||||
self.ignores.insert(parent_path, (ignore, self.scan_id));
|
||||
self.ignores_by_parent_abs_path.insert(
|
||||
self.abs_path.join(&parent_path).into(),
|
||||
(ignore, self.scan_id),
|
||||
);
|
||||
}
|
||||
if matches!(parent_entry.kind, EntryKind::PendingDir) {
|
||||
parent_entry.kind = EntryKind::Dir;
|
||||
@@ -1472,16 +1476,20 @@ impl LocalSnapshot {
|
||||
self.entries_by_id.edit(entries_by_id_edits, &());
|
||||
|
||||
if path.file_name() == Some(&GITIGNORE) {
|
||||
if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) {
|
||||
let abs_parent_path = self.abs_path.join(path.parent().unwrap());
|
||||
if let Some((_, scan_id)) = self
|
||||
.ignores_by_parent_abs_path
|
||||
.get_mut(abs_parent_path.as_path())
|
||||
{
|
||||
*scan_id = self.snapshot.scan_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ignore_stack_for_path(&self, path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||
let mut new_ignores = Vec::new();
|
||||
for ancestor in path.ancestors().skip(1) {
|
||||
if let Some((ignore, _)) = self.ignores.get(ancestor) {
|
||||
for ancestor in abs_path.ancestors().skip(1) {
|
||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||
} else {
|
||||
new_ignores.push((ancestor, None));
|
||||
@@ -1489,16 +1497,16 @@ impl LocalSnapshot {
|
||||
}
|
||||
|
||||
let mut ignore_stack = IgnoreStack::none();
|
||||
for (parent_path, ignore) in new_ignores.into_iter().rev() {
|
||||
if ignore_stack.is_path_ignored(&parent_path, true) {
|
||||
for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
|
||||
if ignore_stack.is_abs_path_ignored(&parent_abs_path, true) {
|
||||
ignore_stack = IgnoreStack::all();
|
||||
break;
|
||||
} else if let Some(ignore) = ignore {
|
||||
ignore_stack = ignore_stack.append(Arc::from(parent_path), ignore);
|
||||
ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
|
||||
}
|
||||
}
|
||||
|
||||
if ignore_stack.is_path_ignored(path, is_dir) {
|
||||
if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
|
||||
ignore_stack = IgnoreStack::all();
|
||||
}
|
||||
|
||||
@@ -1506,8 +1514,8 @@ impl LocalSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
||||
let contents = smol::block_on(fs.load(&abs_path))?;
|
||||
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
||||
let contents = fs.load(&abs_path).await?;
|
||||
let parent = abs_path.parent().unwrap_or(Path::new("/"));
|
||||
let mut builder = GitignoreBuilder::new(parent);
|
||||
for line in contents.lines() {
|
||||
@@ -2040,24 +2048,48 @@ impl BackgroundScanner {
|
||||
|
||||
async fn scan_dirs(&mut self) -> Result<()> {
|
||||
let root_char_bag;
|
||||
let root_abs_path;
|
||||
let next_entry_id;
|
||||
let is_dir;
|
||||
{
|
||||
let snapshot = self.snapshot.lock();
|
||||
root_char_bag = snapshot.root_char_bag;
|
||||
root_abs_path = snapshot.abs_path.clone();
|
||||
next_entry_id = snapshot.next_entry_id.clone();
|
||||
is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir())
|
||||
};
|
||||
|
||||
// Populate ignores above the root.
|
||||
for ancestor in root_abs_path.ancestors().skip(1) {
|
||||
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.snapshot
|
||||
.lock()
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
let ignore_stack = {
|
||||
let mut snapshot = self.snapshot.lock();
|
||||
let ignore_stack = snapshot.ignore_stack_for_abs_path(&root_abs_path, true);
|
||||
if ignore_stack.is_all() {
|
||||
if let Some(mut root_entry) = snapshot.root_entry().cloned() {
|
||||
root_entry.is_ignored = true;
|
||||
snapshot.insert_entry(root_entry, self.fs.as_ref());
|
||||
}
|
||||
}
|
||||
ignore_stack
|
||||
};
|
||||
|
||||
if is_dir {
|
||||
let path: Arc<Path> = Arc::from(Path::new(""));
|
||||
let abs_path = self.abs_path();
|
||||
let (tx, rx) = channel::unbounded();
|
||||
self.executor
|
||||
.block(tx.send(ScanJob {
|
||||
abs_path: abs_path.to_path_buf(),
|
||||
abs_path: root_abs_path.to_path_buf(),
|
||||
path,
|
||||
ignore_stack: IgnoreStack::none(),
|
||||
ignore_stack,
|
||||
scan_queue: tx.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -2117,10 +2149,11 @@ impl BackgroundScanner {
|
||||
|
||||
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
||||
if child_name == *GITIGNORE {
|
||||
match build_gitignore(&child_abs_path, self.fs.as_ref()) {
|
||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||
Ok(ignore) => {
|
||||
let ignore = Arc::new(ignore);
|
||||
ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
|
||||
ignore_stack =
|
||||
ignore_stack.append(job.abs_path.as_path().into(), ignore.clone());
|
||||
new_ignore = Some(ignore);
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -2138,7 +2171,9 @@ impl BackgroundScanner {
|
||||
// new jobs as well.
|
||||
let mut new_jobs = new_jobs.iter_mut();
|
||||
for entry in &mut new_entries {
|
||||
entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
|
||||
let entry_abs_path = self.abs_path().join(&entry.path);
|
||||
entry.is_ignored =
|
||||
ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
|
||||
if entry.is_dir() {
|
||||
new_jobs.next().unwrap().ignore_stack = if entry.is_ignored {
|
||||
IgnoreStack::all()
|
||||
@@ -2157,7 +2192,7 @@ impl BackgroundScanner {
|
||||
);
|
||||
|
||||
if child_metadata.is_dir {
|
||||
let is_ignored = ignore_stack.is_path_ignored(&child_path, true);
|
||||
let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
|
||||
child_entry.is_ignored = is_ignored;
|
||||
new_entries.push(child_entry);
|
||||
new_jobs.push(ScanJob {
|
||||
@@ -2171,7 +2206,7 @@ impl BackgroundScanner {
|
||||
scan_queue: job.scan_queue.clone(),
|
||||
});
|
||||
} else {
|
||||
child_entry.is_ignored = ignore_stack.is_path_ignored(&child_path, false);
|
||||
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
|
||||
new_entries.push(child_entry);
|
||||
};
|
||||
}
|
||||
@@ -2200,8 +2235,8 @@ impl BackgroundScanner {
|
||||
next_entry_id = snapshot.next_entry_id.clone();
|
||||
}
|
||||
|
||||
let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await {
|
||||
abs_path
|
||||
let root_canonical_path = if let Ok(path) = self.fs.canonicalize(&root_abs_path).await {
|
||||
path
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
@@ -2221,27 +2256,29 @@ impl BackgroundScanner {
|
||||
let mut snapshot = self.snapshot.lock();
|
||||
snapshot.scan_id += 1;
|
||||
for event in &events {
|
||||
if let Ok(path) = event.path.strip_prefix(&root_abs_path) {
|
||||
if let Ok(path) = event.path.strip_prefix(&root_canonical_path) {
|
||||
snapshot.remove_path(&path);
|
||||
}
|
||||
}
|
||||
|
||||
for (event, metadata) in events.into_iter().zip(metadata.into_iter()) {
|
||||
let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
|
||||
let path: Arc<Path> = match event.path.strip_prefix(&root_canonical_path) {
|
||||
Ok(path) => Arc::from(path.to_path_buf()),
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
"unexpected event {:?} for root path {:?}",
|
||||
event.path,
|
||||
root_abs_path
|
||||
root_canonical_path
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let abs_path = root_abs_path.join(&path);
|
||||
|
||||
match metadata {
|
||||
Ok(Some(metadata)) => {
|
||||
let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
|
||||
let ignore_stack =
|
||||
snapshot.ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
|
||||
let mut fs_entry = Entry::new(
|
||||
path.clone(),
|
||||
&metadata,
|
||||
@@ -2253,7 +2290,7 @@ impl BackgroundScanner {
|
||||
if metadata.is_dir {
|
||||
self.executor
|
||||
.block(scan_queue_tx.send(ScanJob {
|
||||
abs_path: event.path,
|
||||
abs_path,
|
||||
path,
|
||||
ignore_stack,
|
||||
scan_queue: scan_queue_tx.clone(),
|
||||
@@ -2301,37 +2338,42 @@ impl BackgroundScanner {
|
||||
|
||||
let mut ignores_to_update = Vec::new();
|
||||
let mut ignores_to_delete = Vec::new();
|
||||
for (parent_path, (_, scan_id)) in &snapshot.ignores {
|
||||
if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
|
||||
ignores_to_update.push(parent_path.clone());
|
||||
}
|
||||
for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path {
|
||||
if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) {
|
||||
if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
|
||||
ignores_to_update.push(parent_abs_path.clone());
|
||||
}
|
||||
|
||||
let ignore_path = parent_path.join(&*GITIGNORE);
|
||||
if snapshot.entry_for_path(ignore_path).is_none() {
|
||||
ignores_to_delete.push(parent_path.clone());
|
||||
let ignore_path = parent_path.join(&*GITIGNORE);
|
||||
if snapshot.entry_for_path(ignore_path).is_none() {
|
||||
ignores_to_delete.push(parent_abs_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for parent_path in ignores_to_delete {
|
||||
snapshot.ignores.remove(&parent_path);
|
||||
self.snapshot.lock().ignores.remove(&parent_path);
|
||||
for parent_abs_path in ignores_to_delete {
|
||||
snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path);
|
||||
self.snapshot
|
||||
.lock()
|
||||
.ignores_by_parent_abs_path
|
||||
.remove(&parent_abs_path);
|
||||
}
|
||||
|
||||
let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
|
||||
ignores_to_update.sort_unstable();
|
||||
let mut ignores_to_update = ignores_to_update.into_iter().peekable();
|
||||
while let Some(parent_path) = ignores_to_update.next() {
|
||||
while let Some(parent_abs_path) = ignores_to_update.next() {
|
||||
while ignores_to_update
|
||||
.peek()
|
||||
.map_or(false, |p| p.starts_with(&parent_path))
|
||||
.map_or(false, |p| p.starts_with(&parent_abs_path))
|
||||
{
|
||||
ignores_to_update.next().unwrap();
|
||||
}
|
||||
|
||||
let ignore_stack = snapshot.ignore_stack_for_path(&parent_path, true);
|
||||
let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
|
||||
ignore_queue_tx
|
||||
.send(UpdateIgnoreStatusJob {
|
||||
path: parent_path,
|
||||
abs_path: parent_abs_path,
|
||||
ignore_stack,
|
||||
ignore_queue: ignore_queue_tx.clone(),
|
||||
})
|
||||
@@ -2355,15 +2397,17 @@ impl BackgroundScanner {
|
||||
|
||||
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||
let mut ignore_stack = job.ignore_stack;
|
||||
if let Some((ignore, _)) = snapshot.ignores.get(&job.path) {
|
||||
ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
|
||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||
ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
|
||||
}
|
||||
|
||||
let mut entries_by_id_edits = Vec::new();
|
||||
let mut entries_by_path_edits = Vec::new();
|
||||
for mut entry in snapshot.child_entries(&job.path).cloned() {
|
||||
let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
|
||||
for mut entry in snapshot.child_entries(path).cloned() {
|
||||
let was_ignored = entry.is_ignored;
|
||||
entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
|
||||
let abs_path = self.abs_path().join(&entry.path);
|
||||
entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
|
||||
if entry.is_dir() {
|
||||
let child_ignore_stack = if entry.is_ignored {
|
||||
IgnoreStack::all()
|
||||
@@ -2372,7 +2416,7 @@ impl BackgroundScanner {
|
||||
};
|
||||
job.ignore_queue
|
||||
.send(UpdateIgnoreStatusJob {
|
||||
path: entry.path.clone(),
|
||||
abs_path: abs_path.into(),
|
||||
ignore_stack: child_ignore_stack,
|
||||
ignore_queue: job.ignore_queue.clone(),
|
||||
})
|
||||
@@ -2413,7 +2457,7 @@ struct ScanJob {
|
||||
}
|
||||
|
||||
struct UpdateIgnoreStatusJob {
|
||||
path: Arc<Path>,
|
||||
abs_path: Arc<Path>,
|
||||
ignore_stack: Arc<IgnoreStack>,
|
||||
ignore_queue: Sender<UpdateIgnoreStatusJob>,
|
||||
}
|
||||
@@ -2766,23 +2810,28 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "tracked contents",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": "ignored contents",
|
||||
let parent_dir = temp_tree(json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "",
|
||||
"ancestor-ignored-file1": "",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": ""
|
||||
}
|
||||
}
|
||||
}));
|
||||
let dir = parent_dir.path().join("tree");
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
dir.path(),
|
||||
dir.as_path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
@@ -2795,23 +2844,47 @@ mod tests {
|
||||
tree.flush_fs_events(&cx).await;
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
|
||||
assert_eq!(tracked.is_ignored, false);
|
||||
assert_eq!(ignored.is_ignored, true);
|
||||
assert!(
|
||||
!tree
|
||||
.entry_for_path("tracked-dir/tracked-file1")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("ignored-dir/ignored-file1")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
std::fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
|
||||
std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
|
||||
std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
|
||||
tree.flush_fs_events(&cx).await;
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let dot_git = tree.entry_for_path(".git").unwrap();
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
|
||||
assert_eq!(tracked.is_ignored, false);
|
||||
assert_eq!(ignored.is_ignored, true);
|
||||
assert_eq!(dot_git.is_ignored, true);
|
||||
assert!(
|
||||
!tree
|
||||
.entry_for_path("tracked-dir/tracked-file2")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
assert!(
|
||||
tree.entry_for_path("ignored-dir/ignored-file2")
|
||||
.unwrap()
|
||||
.is_ignored
|
||||
);
|
||||
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2891,7 +2964,7 @@ mod tests {
|
||||
let mut initial_snapshot = LocalSnapshot {
|
||||
abs_path: root_dir.path().into(),
|
||||
removed_entry_ids: Default::default(),
|
||||
ignores: Default::default(),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
next_entry_id: next_entry_id.clone(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(0),
|
||||
@@ -3176,8 +3249,10 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
|
||||
|
||||
for (ignore_parent_path, _) in &self.ignores {
|
||||
assert!(self.entry_for_path(ignore_parent_path).is_some());
|
||||
for (ignore_parent_abs_path, _) in &self.ignores_by_parent_abs_path {
|
||||
let ignore_parent_path =
|
||||
ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
|
||||
assert!(self.entry_for_path(&ignore_parent_path).is_some());
|
||||
assert!(self
|
||||
.entry_for_path(ignore_parent_path.join(&*GITIGNORE))
|
||||
.is_some());
|
||||
|
||||
Reference in New Issue
Block a user