diff --git a/Cargo.lock b/Cargo.lock index 7ab4a85c7d..7fdc2e7fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11474,6 +11474,7 @@ name = "paths" version = "0.1.0" dependencies = [ "dirs 4.0.0", + "ignore", "util", "workspace-hack", ] diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index a76ccee2bf..495f596a5c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -131,7 +131,6 @@ pub trait Fs: Send + Sync { Arc, ); - fn home_dir(&self) -> Option; fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>; fn is_fake(&self) -> bool; @@ -879,10 +878,6 @@ impl Fs for RealFs { temp_dir.close()?; case_sensitive } - - fn home_dir(&self) -> Option { - Some(paths::home_dir().clone()) - } } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] @@ -917,7 +912,6 @@ struct FakeFsState { read_dir_call_count: usize, path_write_counts: std::collections::HashMap, moves: std::collections::HashMap, - home_dir: Option, } #[cfg(any(test, feature = "test-support"))] @@ -1104,7 +1098,6 @@ impl FakeFs { metadata_call_count: 0, path_write_counts: Default::default(), moves: Default::default(), - home_dir: None, })), }); @@ -1763,10 +1756,6 @@ impl FakeFs { fn simulate_random_delay(&self) -> impl futures::Future { self.executor.simulate_random_delay() } - - pub fn set_home_dir(&self, home_dir: PathBuf) { - self.state.lock().home_dir = Some(home_dir); - } } #[cfg(any(test, feature = "test-support"))] @@ -2364,10 +2353,6 @@ impl Fs for FakeFs { fn as_fake(&self) -> Arc { self.this.upgrade().unwrap() } - - fn home_dir(&self) -> Option { - self.state.lock().home_dir.clone() - } } fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator { diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml index cf6dabf0e1..44bb0953e2 100644 --- a/crates/paths/Cargo.toml +++ b/crates/paths/Cargo.toml @@ -8,10 +8,14 @@ license = "GPL-3.0-or-later" [lints] workspace = true +[features] +test-support = [] + [lib] path = "src/paths.rs" [dependencies] dirs.workspace = true +ignore.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 2f3b188980..f929747ee6 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -507,3 +507,16 @@ fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { ); } } + +#[cfg(any(test, feature = "test-support"))] +pub fn global_gitignore_path() -> Option { + Some(Path::new(util::path!("/home/zed/.config/git/ignore")).into()) +} + +#[cfg(not(any(test, feature = "test-support")))] +pub fn global_gitignore_path() -> Option { + static GLOBAL_GITIGNORE_PATH: OnceLock> = OnceLock::new(); + GLOBAL_GITIGNORE_PATH + .get_or_init(::ignore::gitignore::gitconfig_excludes_path) + .clone() +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 779cf95add..fa83d79004 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -29,7 +29,7 @@ use lsp::{ WillRenameFiles, notification::DidRenameFiles, }; use parking_lot::Mutex; -use paths::{config_dir, tasks_file}; +use paths::{config_dir, global_gitignore_path, tasks_file}; use postage::stream::Stream as _; use pretty_assertions::{assert_eq, assert_matches}; use rand::{Rng as _, rngs::StdRng}; @@ -1179,7 +1179,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5); let mut new_watched_paths = fs.watched_paths(); - new_watched_paths.retain(|path| !path.starts_with(config_dir())); + new_watched_paths.retain(|path| { + !path.starts_with(config_dir()) && !path.starts_with(global_gitignore_path().unwrap()) + }); assert_eq!( &new_watched_paths, &[ @@ -7716,9 +7718,9 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - path!("/root"), + path!("/home"), json!({ - "home": { + "zed": { ".git": {}, "project": { "a.txt": "A" @@ -7727,9 +7729,8 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) { }), ) .await; - fs.set_home_dir(Path::new(path!("/root/home")).to_owned()); - let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/home/zed/project").as_ref()], cx).await; let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let tree_id = tree.read_with(cx, |tree, _| tree.id()); @@ -7746,7 +7747,7 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) { assert!(containing.is_none()); }); - let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/home/zed").as_ref()], cx).await; let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let tree_id = tree.read_with(cx, |tree, _| tree.id()); project @@ -7766,7 +7767,7 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) { .read(cx) .work_directory_abs_path .as_ref(), - Path::new(path!("/root/home")) + Path::new(path!("/home/zed")) ); }); } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 585f2b08aa..0d310073d1 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -16,7 +16,13 @@ use crate::NumericPrefixWithSuffix; /// Returns the path to the user's home directory. pub fn home_dir() -> &'static PathBuf { static HOME_DIR: OnceLock = OnceLock::new(); - HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory")) + HOME_DIR.get_or_init(|| { + if cfg!(any(test, feature = "test-support")) { + PathBuf::from("/home/zed") + } else { + dirs::home_dir().expect("failed to determine home directory") + } + }) } pub trait PathExt { diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index db264fe3aa..507b95c00d 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -55,6 +55,7 @@ collections = { workspace = true, features = ["test-support"] } git2.workspace = true gpui = { workspace = true, features = ["test-support"] } http_client.workspace = true +paths = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true rand.workspace = true rpc = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index e8ba9192be..17c362e2d7 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -1,34 +1,60 @@ use ignore::gitignore::Gitignore; use std::{ffi::OsStr, path::Path, sync::Arc}; +#[derive(Clone, Debug)] +pub struct IgnoreStack { + pub repo_root: Option>, + pub top: Arc, +} + #[derive(Debug)] -pub enum IgnoreStack { +pub enum IgnoreStackEntry { None, + Global { + ignore: Arc, + }, Some { abs_base_path: Arc, ignore: Arc, - parent: Arc, + parent: Arc, }, All, } impl IgnoreStack { - pub fn none() -> Arc { - Arc::new(Self::None) + pub fn none() -> Self { + Self { + repo_root: None, + top: Arc::new(IgnoreStackEntry::None), + } } - pub fn all() -> Arc { - Arc::new(Self::All) + pub fn all() -> Self { + Self { + repo_root: None, + top: Arc::new(IgnoreStackEntry::All), + } } - pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { - match self.as_ref() { - IgnoreStack::All => self, - _ => Arc::new(Self::Some { + pub fn global(ignore: Arc) -> Self { + Self { + repo_root: None, + top: Arc::new(IgnoreStackEntry::Global { ignore }), + } + } + + pub fn append(self, abs_base_path: Arc, ignore: Arc) -> Self { + let top = match self.top.as_ref() { + IgnoreStackEntry::All => self.top.clone(), + _ => Arc::new(IgnoreStackEntry::Some { abs_base_path, ignore, - parent: self, + parent: self.top.clone(), }), + }; + Self { + repo_root: self.repo_root, + top, } } @@ -37,15 +63,37 @@ impl IgnoreStack { return true; } - match self { - Self::None => false, - Self::All => true, - Self::Some { + match self.top.as_ref() { + IgnoreStackEntry::None => false, + IgnoreStackEntry::All => true, + IgnoreStackEntry::Global { ignore } => { + let combined_path; + let abs_path = if let Some(repo_root) = self.repo_root.as_ref() { + combined_path = ignore.path().join( + abs_path + .strip_prefix(repo_root) + .expect("repo root should be a parent of matched path"), + ); + &combined_path + } else { + abs_path + }; + match ignore.matched(abs_path, is_dir) { + ignore::Match::None => false, + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + } + } + IgnoreStackEntry::Some { abs_base_path, ignore, parent: prev, } => 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::None => IgnoreStack { + repo_root: self.repo_root.clone(), + top: prev.clone(), + } + .is_abs_path_ignored(abs_path, is_dir), ignore::Match::Ignore(_) => true, ignore::Match::Whitelist(_) => false, }, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e6949f62df..1846f0909b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -65,7 +65,7 @@ use std::{ use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ - ResultExt, + ResultExt, debug_panic, paths::{PathMatcher, SanitizedPath, home_dir}, }; pub use worktree_settings::WorktreeSettings; @@ -336,26 +336,10 @@ impl Default for WorkDirectory { } } -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct WorkDirectoryEntry(ProjectEntryId); - -impl Deref for WorkDirectoryEntry { - type Target = ProjectEntryId; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for WorkDirectoryEntry { - fn from(value: ProjectEntryId) -> Self { - WorkDirectoryEntry(value) - } -} - #[derive(Debug, Clone)] pub struct LocalSnapshot { snapshot: Snapshot, + global_gitignore: Option>, /// All of the gitignore files in the worktree, indexed by their relative path. /// The boolean indicates whether the gitignore needs to be updated. ignores_by_parent_abs_path: HashMap, (Arc, bool)>, @@ -504,6 +488,7 @@ impl Worktree { cx.new(move |cx: &mut Context| { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), + global_gitignore: Default::default(), git_repositories: Default::default(), snapshot: Snapshot::new( cx.entity_id().as_u64(), @@ -2785,8 +2770,9 @@ impl LocalSnapshot { inodes } - fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { + fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool, fs: &dyn Fs) -> IgnoreStack { let mut new_ignores = Vec::new(); + let mut repo_root = None; for (index, ancestor) in abs_path.ancestors().enumerate() { if index > 0 { if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { @@ -2795,12 +2781,21 @@ impl LocalSnapshot { new_ignores.push((ancestor, None)); } } - if ancestor.join(*DOT_GIT).exists() { + let metadata = smol::block_on(fs.metadata(&ancestor.join(*DOT_GIT))) + .ok() + .flatten(); + if metadata.is_some() { + repo_root = Some(Arc::from(ancestor)); break; } } - let mut ignore_stack = IgnoreStack::none(); + let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { + IgnoreStack::global(global_gitignore) + } else { + IgnoreStack::none() + }; + ignore_stack.repo_root = repo_root; 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(); @@ -2927,9 +2922,15 @@ impl BackgroundScannerState { .any(|p| entry.path.starts_with(p)) } - fn enqueue_scan_dir(&self, abs_path: Arc, entry: &Entry, scan_job_tx: &Sender) { + fn enqueue_scan_dir( + &self, + abs_path: Arc, + entry: &Entry, + scan_job_tx: &Sender, + fs: &dyn Fs, + ) { let path = entry.path.clone(); - let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true, fs); let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); if !ancestor_inodes.contains(&entry.inode) { @@ -3845,19 +3846,46 @@ impl BackgroundScanner { log::trace!("containing git repository: {containing_git_repository:?}"); + let global_gitignore_path = paths::global_gitignore_path(); + self.state.lock().snapshot.global_gitignore = + if let Some(global_gitignore_path) = global_gitignore_path.as_ref() { + build_gitignore(global_gitignore_path, self.fs.as_ref()) + .await + .log_err() + .map(Arc::new) + } else { + None + }; + let mut global_gitignore_events = if let Some(global_gitignore_path) = global_gitignore_path + { + self.fs + .watch(&global_gitignore_path, FS_WATCH_LATENCY) + .await + .0 + } else { + Box::pin(futures::stream::empty()) + }; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); { let mut state = self.state.lock(); state.snapshot.scan_id += 1; if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(root_abs_path.as_path(), true); + let ignore_stack = state.snapshot.ignore_stack_for_abs_path( + root_abs_path.as_path(), + true, + self.fs.as_ref(), + ); if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { root_entry.is_ignored = true; state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } - state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx); + state.enqueue_scan_dir( + root_abs_path.into(), + &root_entry, + &scan_job_tx, + self.fs.as_ref(), + ); } }; @@ -3927,6 +3955,15 @@ impl BackgroundScanner { } self.process_events(paths.into_iter().map(Into::into).collect()).await; } + + paths = global_gitignore_events.next().fuse() => { + match paths.as_deref() { + Some([event, ..]) => { + self.update_global_gitignore(&event.path).await; + } + _ => {}, + } + } } } } @@ -4109,11 +4146,20 @@ impl BackgroundScanner { ) .await; - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + let affected_repo_roots = if !dot_git_abs_paths.is_empty() { + self.update_git_repositories(dot_git_abs_paths) + } else { + Vec::new() + }; - if !dot_git_abs_paths.is_empty() { - self.update_git_repositories(dot_git_abs_paths); + { + let mut ignores_to_update = self.ignores_needing_update(); + ignores_to_update.extend(affected_repo_roots); + let ignores_to_update = self.order_ignores(ignores_to_update); + let snapshot = self.state.lock().snapshot.clone(); + self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update) + .await; + self.scan_dirs(false, scan_job_rx).await; } { @@ -4126,6 +4172,32 @@ impl BackgroundScanner { self.send_status_update(false, SmallVec::new()); } + async fn update_global_gitignore(&self, abs_path: &Path) { + let ignore = build_gitignore(abs_path, self.fs.as_ref()) + .await + .log_err() + .map(Arc::new); + let (prev_snapshot, ignore_stack, abs_path) = { + let mut state = self.state.lock(); + state.snapshot.global_gitignore = ignore; + let abs_path = state.snapshot.abs_path().clone(); + let ignore_stack = + state + .snapshot + .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()); + (state.snapshot.clone(), ignore_stack, abs_path) + }; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses_for_paths( + scan_job_tx, + prev_snapshot, + vec![(abs_path, ignore_stack)].into_iter(), + ) + .await; + self.scan_dirs(false, scan_job_rx).await; + self.send_status_update(false, SmallVec::new()); + } + async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { let (scan_job_tx, scan_job_rx) = channel::unbounded(); { @@ -4136,7 +4208,12 @@ impl BackgroundScanner { if let Some(entry) = state.snapshot.entry_for_path(ancestor) { if entry.kind == EntryKind::UnloadedDir { let abs_path = root_path.as_path().join(ancestor); - state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); + state.enqueue_scan_dir( + abs_path.into(), + entry, + &scan_job_tx, + self.fs.as_ref(), + ); state.paths_to_scan.insert(path.clone()); break; } @@ -4289,6 +4366,12 @@ impl BackgroundScanner { swap_to_front(&mut child_paths, *GITIGNORE); swap_to_front(&mut child_paths, *DOT_GIT); + if let Some(path) = child_paths.first() + && path.ends_with(*DOT_GIT) + { + ignore_stack.repo_root = Some(job.abs_path.clone()); + } + for child_abs_path in child_paths { let child_abs_path: Arc = child_abs_path.into(); let child_name = child_abs_path.file_name().unwrap(); @@ -4514,9 +4597,11 @@ impl BackgroundScanner { let abs_path: Arc = root_abs_path.as_path().join(path).into(); match metadata { Ok(Some((metadata, canonical_path))) => { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, metadata.is_dir); + let ignore_stack = state.snapshot.ignore_stack_for_abs_path( + &abs_path, + metadata.is_dir, + self.fs.as_ref(), + ); let is_external = !canonical_path.starts_with(&root_canonical_path); let mut fs_entry = Entry::new( path.clone(), @@ -4541,7 +4626,12 @@ impl BackgroundScanner { || (fs_entry.path.as_os_str().is_empty() && abs_path.file_name() == Some(*DOT_GIT)) { - state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx); + state.enqueue_scan_dir( + abs_path, + &fs_entry, + scan_queue_tx, + self.fs.as_ref(), + ); } else { fs_entry.kind = EntryKind::UnloadedDir; } @@ -4597,43 +4687,15 @@ impl BackgroundScanner { Some(()) } - async fn update_ignore_statuses(&self, scan_job_tx: Sender) { - let mut ignores_to_update = Vec::new(); + async fn update_ignore_statuses_for_paths( + &self, + scan_job_tx: Sender, + prev_snapshot: LocalSnapshot, + mut ignores_to_update: impl Iterator, IgnoreStack)>, + ) { let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); - let prev_snapshot; { - let snapshot = &mut self.state.lock().snapshot; - let abs_path = snapshot.abs_path.clone(); - snapshot - .ignores_by_parent_abs_path - .retain(|parent_abs_path, (_, needs_update)| { - if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) { - if *needs_update { - *needs_update = false; - if snapshot.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.snapshot.entry_for_path(ignore_path).is_none() { - return false; - } - } - true - }); - - ignores_to_update.sort_unstable(); - let mut ignores_to_update = ignores_to_update.into_iter().peekable(); - while let Some(parent_abs_path) = ignores_to_update.next() { - while ignores_to_update - .peek() - .map_or(false, |p| p.starts_with(&parent_abs_path)) - { - ignores_to_update.next().unwrap(); - } - - let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); + while let Some((parent_abs_path, ignore_stack)) = ignores_to_update.next() { ignore_queue_tx .send_blocking(UpdateIgnoreStatusJob { abs_path: parent_abs_path, @@ -4643,8 +4705,6 @@ impl BackgroundScanner { }) .unwrap(); } - - prev_snapshot = snapshot.clone(); } drop(ignore_queue_tx); @@ -4676,6 +4736,57 @@ impl BackgroundScanner { .await; } + fn ignores_needing_update(&self) -> Vec> { + let mut ignores_to_update = Vec::new(); + + { + let snapshot = &mut self.state.lock().snapshot; + let abs_path = snapshot.abs_path.clone(); + snapshot + .ignores_by_parent_abs_path + .retain(|parent_abs_path, (_, needs_update)| { + if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) { + if *needs_update { + *needs_update = false; + if snapshot.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.snapshot.entry_for_path(ignore_path).is_none() { + return false; + } + } + true + }); + } + + ignores_to_update + } + + fn order_ignores( + &self, + mut ignores: Vec>, + ) -> impl use<> + Iterator, IgnoreStack)> { + let fs = self.fs.clone(); + let snapshot = self.state.lock().snapshot.clone(); + ignores.sort_unstable(); + let mut ignores_to_update = ignores.into_iter().peekable(); + std::iter::from_fn(move || { + let parent_abs_path = ignores_to_update.next()?; + while ignores_to_update + .peek() + .map_or(false, |p| p.starts_with(&parent_abs_path)) + { + ignores_to_update.next().unwrap(); + } + let ignore_stack = + snapshot.ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref()); + Some((parent_abs_path, ignore_stack)) + }) + } + async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { log::trace!("update ignore status {:?}", job.abs_path); @@ -4691,6 +4802,12 @@ impl BackgroundScanner { .strip_prefix(snapshot.abs_path.as_path()) .unwrap(); + if let Ok(Some(metadata)) = smol::block_on(self.fs.metadata(&job.abs_path.join(*DOT_GIT))) + && metadata.is_dir + { + ignore_stack.repo_root = Some(job.abs_path.clone()); + } + for mut entry in snapshot.child_entries(path).cloned() { let was_ignored = entry.is_ignored; let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); @@ -4707,7 +4824,12 @@ impl BackgroundScanner { if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { let state = self.state.lock(); if state.should_scan_directory(&entry) { - state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue); + state.enqueue_scan_dir( + abs_path.clone(), + &entry, + &job.scan_queue, + self.fs.as_ref(), + ); } } @@ -4747,10 +4869,11 @@ impl BackgroundScanner { state.snapshot.entries_by_id.edit(entries_by_id_edits, &()); } - fn update_git_repositories(&self, dot_git_paths: Vec) { + fn update_git_repositories(&self, dot_git_paths: Vec) -> Vec> { log::trace!("reloading repositories: {dot_git_paths:?}"); let mut state = self.state.lock(); let scan_id = state.snapshot.scan_id; + let mut affected_repo_roots = Vec::new(); for dot_git_dir in dot_git_paths { let existing_repository_entry = state @@ -4770,8 +4893,12 @@ impl BackgroundScanner { match existing_repository_entry { None => { let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else { - return; + debug_panic!( + "update_git_repositories called with .git directory outside the worktree root" + ); + return Vec::new(); }; + affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); state.insert_git_repository( relative.into(), self.fs.as_ref(), @@ -4811,7 +4938,15 @@ impl BackgroundScanner { snapshot .git_repositories - .retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id)); + .retain(|work_directory_id, entry| { + let preserve = ids_to_preserve.contains(work_directory_id); + if !preserve { + affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into()); + } + preserve + }); + + affected_repo_roots } async fn progress_timer(&self, running: bool) { @@ -4851,7 +4986,7 @@ async fn discover_ancestor_git_repo( let mut ignores = HashMap::default(); for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { if index != 0 { - if Some(ancestor) == fs.home_dir().as_deref() { + if ancestor == paths::home_dir() { // Unless $HOME is itself the worktree root, don't consider it as a // containing git repository---expensive and likely unwanted. break; @@ -5033,7 +5168,7 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { struct ScanJob { abs_path: Arc, path: Arc, - ignore_stack: Arc, + ignore_stack: IgnoreStack, scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, @@ -5041,7 +5176,7 @@ struct ScanJob { struct UpdateIgnoreStatusJob { abs_path: Arc, - ignore_stack: Arc, + ignore_stack: IgnoreStack, ignore_queue: Sender, scan_queue: Sender, } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d4c309e5bc..65e0703ab2 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2025,7 +2025,6 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA }); pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); - eprintln!(">>>>>>>>>> touch"); fs.touch_path(path!("/root/subproject")).await; worktree .update(cx, |worktree, _| { @@ -2046,6 +2045,116 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); } +#[gpui::test] +async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/home/zed"), + json!({ + ".config": { + "git": { + "ignore": "foo\n/bar\nbaz\n" + } + }, + "project": { + ".git": {}, + ".gitignore": "!baz", + "foo": "", + "bar": "", + "sub": { + "bar": "", + }, + "subrepo": { + ".git": {}, + "bar": "" + }, + "baz": "" + } + }), + ) + .await; + let worktree = Worktree::local( + path!("/home/zed/project").as_ref(), + true, + fs.clone(), + Arc::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved + // relative to the nearest containing repository + worktree.update(cx, |worktree, _cx| { + check_worktree_entries( + worktree, + &[], + &["foo", "bar", "subrepo/bar"], + &["sub/bar", "baz"], + &[], + ); + }); + + // Ignore statuses are updated when excludesFile changes + fs.write( + Path::new(path!("/home/zed/.config/git/ignore")), + "/bar\nbaz\n".as_bytes(), + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + worktree.update(cx, |worktree, _cx| { + check_worktree_entries( + worktree, + &[], + &["bar", "subrepo/bar"], + &["foo", "sub/bar", "baz"], + &[], + ); + }); + + // Statuses are updated when .git added/removed + fs.remove_dir( + Path::new(path!("/home/zed/project/subrepo/.git")), + RemoveOptions { + recursive: true, + ..Default::default() + }, + ) + .await + .unwrap(); + worktree + .update(cx, |worktree, _| { + worktree.as_local().unwrap().scan_complete() + }) + .await; + cx.run_until_parked(); + + worktree.update(cx, |worktree, _cx| { + check_worktree_entries( + worktree, + &[], + &["bar"], + &["foo", "sub/bar", "baz", "subrepo/bar"], + &[], + ); + }); +} + #[track_caller] fn check_worktree_entries( tree: &Worktree,