diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index db225f628e..6ce338310d 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -131,7 +131,7 @@ pub trait Fs: Send + Sync { Arc, ); - fn home_dir(&self) -> Option; + // 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 global_git_ignore_path(&self, abs_work_directory: &Path) -> Option; @@ -876,9 +876,9 @@ impl Fs for RealFs { case_sensitive } - fn home_dir(&self) -> Option { - Some(paths::home_dir().clone()) - } + // fn home_dir(&self) -> Option { + // Some(paths::home_dir().clone()) + // } } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] @@ -912,7 +912,7 @@ struct FakeFsState { metadata_call_count: usize, read_dir_call_count: usize, moves: std::collections::HashMap, - home_dir: Option, + // home_dir: Option, } #[cfg(any(test, feature = "test-support"))] @@ -1098,7 +1098,7 @@ impl FakeFs { read_dir_call_count: 0, metadata_call_count: 0, moves: Default::default(), - home_dir: None, + // home_dir: None, })), }); @@ -1744,10 +1744,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"))] @@ -2350,9 +2346,9 @@ impl Fs for FakeFs { self.this.upgrade().unwrap() } - fn home_dir(&self) -> Option { - self.state.lock().home_dir.clone() - } + // fn home_dir(&self) -> Option { + // self.state.lock().home_dir.clone() + // } } fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 23bda8bf65..d99b6e42ea 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -7578,9 +7578,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" @@ -7589,9 +7589,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()); @@ -7608,7 +7607,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 @@ -7628,7 +7627,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 47ea662d7d..73c4203014 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!(test) { + PathBuf::from("/home/zed") + } else { + dirs::home_dir().expect("failed to determine home directory") + } + }) } pub trait PathExt { diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index e8ba9192be..89c33fa280 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -4,6 +4,9 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; #[derive(Debug)] pub enum IgnoreStack { None, + Global { + ignore: Arc, + }, Some { abs_base_path: Arc, ignore: Arc, @@ -21,6 +24,10 @@ impl IgnoreStack { Arc::new(Self::All) } + pub fn global(ignore: Arc) -> Arc { + Arc::new(Self::Global { ignore }) + } + pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { match self.as_ref() { IgnoreStack::All => self, @@ -40,6 +47,11 @@ impl IgnoreStack { match self { Self::None => false, Self::All => true, + Self::Global { ignore } => match ignore.matched(abs_path, is_dir) { + ignore::Match::None => false, + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + }, Self::Some { abs_base_path, ignore, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b1c19a2aeb..26d926c3c9 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3,7 +3,7 @@ mod worktree_settings; #[cfg(test)] mod worktree_tests; -use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; +use ::ignore::gitignore::{Gitignore, GitignoreBuilder, gitconfig_excludes_path}; use anyhow::{Context as _, Result, anyhow}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; @@ -65,7 +65,7 @@ use std::{ use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit}; use text::{LineEnding, Rope}; use util::{ - ResultExt, + ResultExt, path, paths::{PathMatcher, SanitizedPath, home_dir}, }; pub use worktree_settings::WorktreeSettings; @@ -356,6 +356,7 @@ impl From for WorkDirectoryEntry { #[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)>, @@ -510,6 +511,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(), @@ -2803,11 +2805,20 @@ impl LocalSnapshot { } } if ancestor.join(*DOT_GIT).exists() { + // FIXME HERE break; } } - let mut ignore_stack = IgnoreStack::none(); + // FIXME the plan for global + // - the abs_base_path for the ignore is "" + // - match relative to dot git parent using existing stripping logic + // - global variant?? + let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { + IgnoreStack::global(global_gitignore) + } else { + IgnoreStack::none() + }; 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(); @@ -3870,6 +3881,26 @@ impl BackgroundScanner { log::trace!("containing git repository: {containing_git_repository:?}"); + let global_gitignore_path = 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(); @@ -3952,6 +3983,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; + } + _ => {}, + } + } } } } @@ -4151,6 +4191,30 @@ 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; + // FIXME is_dir (do we care?) + let abs_path = state.snapshot.abs_path().clone(); + let ignore_stack = state.snapshot.ignore_stack_for_abs_path(&abs_path, true); + (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(); { @@ -4622,43 +4686,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, Arc)>, + ) { 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, @@ -4668,8 +4704,6 @@ impl BackgroundScanner { }) .unwrap(); } - - prev_snapshot = snapshot.clone(); } drop(ignore_queue_tx); @@ -4701,6 +4735,54 @@ impl BackgroundScanner { .await; } + async fn update_ignore_statuses(&self, scan_job_tx: Sender) { + let mut ignores_to_update = Vec::new(); + 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 + }); + + snapshot.clone() + }; + + ignores_to_update.sort_unstable(); + let mut ignores_to_update = ignores_to_update.into_iter().peekable(); + let ignores_to_update = std::iter::from_fn({ + let prev_snapshot = prev_snapshot.clone(); + 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 = prev_snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); + Some((parent_abs_path, ignore_stack)) + } + }); + + self.update_ignore_statuses_for_paths(scan_job_tx, prev_snapshot, ignores_to_update) + .await; + } + async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { log::trace!("update ignore status {:?}", job.abs_path); @@ -4765,6 +4847,9 @@ impl BackgroundScanner { } } + if !entries_by_path_edits.is_empty() { + dbg!(&entries_by_path_edits); + } state .snapshot .entries_by_path @@ -4876,7 +4961,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; @@ -5597,3 +5682,11 @@ fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, (repository_dir_abs_path, common_dir_abs_path) } + +fn global_gitignore_path() -> Option> { + if cfg!(test) { + Some(Path::new(path!("/home/zed/.config/git/ignore")).into()) + } else { + gitconfig_excludes_path().map(Into::into) + } +}