From dfae07e69c8903785212ef5480f7af30985b5174 Mon Sep 17 00:00:00 2001 From: Paul Nameless Date: Tue, 8 Apr 2025 11:19:07 +0200 Subject: [PATCH 01/15] Add global gitignore (~/.config/git/ignore) along with local .gitignore --- crates/worktree/src/worktree.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9b36c6cefc..9af79e128a 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3136,11 +3136,33 @@ async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); + let mut builder = GitignoreBuilder::new(parent); for line in contents.lines() { builder.add_line(Some(abs_path.into()), line)?; } - Ok(builder.build()?) + + builder.build()?; + + let mut combined_builder = GitignoreBuilder::new(parent); + + let home = home_dir(); + if !home.as_os_str().is_empty() { + let global_ignore_path = home.join(".config/git/ignore"); + if let Ok(global_contents) = fs.load(&global_ignore_path).await { + for line in global_contents.lines() { + // Ignore errors for global gitignore entries, as we don't want to block + // local gitignore functionality if the global one has issues + let _ = combined_builder.add_line(Some(global_ignore_path.clone()), line); + } + } + } + + for line in contents.lines() { + combined_builder.add_line(Some(abs_path.into()), line)?; + } + + Ok(combined_builder.build()?) } impl Deref for Worktree { From ac25d05e27ae9b5649fb96f6b81c053f119d16a1 Mon Sep 17 00:00:00 2001 From: Paul Nameless Date: Sun, 13 Apr 2025 16:56:27 +0200 Subject: [PATCH 02/15] Add global and .git/info/exclude ignore files --- crates/worktree/src/worktree.rs | 49 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9af79e128a..cc6e0ef263 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3147,12 +3147,53 @@ async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { let mut combined_builder = GitignoreBuilder::new(parent); let home = home_dir(); - if !home.as_os_str().is_empty() { - let global_ignore_path = home.join(".config/git/ignore"); + let repo_root = abs_path.parent().unwrap_or_else(|| Path::new("/")); + let repo_git_path = repo_root.join(".git"); + let repo_git_config_path = repo_git_path.join("config"); + + let mut global_ignore_path = None; + if let Ok(config_output) = std::process::Command::new("git") + .args(["config", "--file", &repo_git_config_path.to_string_lossy(), "core.excludesFile"]) + .output() + { + if config_output.status.success() { + let path_str = String::from_utf8_lossy(&config_output.stdout).trim().to_string(); + if !path_str.is_empty() { + let path = PathBuf::from(path_str); + global_ignore_path = Some(path); + } + } + } + + if global_ignore_path.is_none() { + if let Ok(config_output) = std::process::Command::new("git") + .args(["config", "--global", "core.excludesFile"]) + .output() + { + if config_output.status.success() { + let path_str = String::from_utf8_lossy(&config_output.stdout).trim().to_string(); + if !path_str.is_empty() { + let path = PathBuf::from(path_str); + global_ignore_path = Some(path); + } + } + } + } + + if global_ignore_path.is_none() && !home.as_os_str().is_empty() { + global_ignore_path = Some(home.join(".config/git/ignore")); + } + + let repo_git_exclude_path = repo_git_path.join("info/exclude"); + if let Ok(exclude_contents) = fs.load(&repo_git_exclude_path).await { + for line in exclude_contents.lines() { + let _ = combined_builder.add_line(Some(repo_git_exclude_path.clone()), line); + } + } + + if let Some(global_ignore_path) = global_ignore_path { if let Ok(global_contents) = fs.load(&global_ignore_path).await { for line in global_contents.lines() { - // Ignore errors for global gitignore entries, as we don't want to block - // local gitignore functionality if the global one has issues let _ = combined_builder.add_line(Some(global_ignore_path.clone()), line); } } From 896248dea3725d3d3c63613816728930d8b48659 Mon Sep 17 00:00:00 2001 From: Paul Nameless Date: Sun, 13 Apr 2025 17:51:55 +0200 Subject: [PATCH 03/15] Add test for gitignore with .git/info/exclude --- crates/worktree/src/worktree_tests.rs | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 45ffc22892..a7eaa4ec43 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1984,6 +1984,58 @@ fn test_unrelativize() { ); } +#[gpui::test] +async fn test_gitignore_with_git_info_exclude(cx: &mut TestAppContext) { + init_test(cx); + + // Test that .git/info/exclude entries are properly recognized + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + ".git": { + "info": { + "exclude": "excluded_file.txt\n", + }, + }, + ".gitignore": "local_ignored.txt\n", + "normal_file.txt": "normal file content", + "local_ignored.txt": "locally ignored content", + "excluded_file.txt": "excluded content", + }), + ) + .await; + + let tree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[], + &[ + "local_ignored.txt", // Ignored by .gitignore + "excluded_file.txt", // Ignored by .git/info/exclude + ], + &[ + "normal_file.txt", // Not ignored + ".gitignore", // Not ignored + ], + &[], + ) + }); +} + #[track_caller] fn check_worktree_entries( tree: &Worktree, From 8792bf839b4faf4130b3e81456e369feeeadc122 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 28 Jun 2025 16:29:29 -0400 Subject: [PATCH 04/15] simplify logic and mock out global gitignore location --- Cargo.lock | 1 + crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 18 +++++++ crates/worktree/src/worktree.rs | 83 ++++++++------------------------- 4 files changed, 39 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d572bd1f78..f4d11830c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5678,6 +5678,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "dirs 4.0.0", "fsevent", "futures 0.3.31", "git", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 633fc1fc99..3c017f607e 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,6 +16,7 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true +dirs.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9adbe495dc..da0b3a3b59 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -134,6 +134,7 @@ pub trait Fs: Send + Sync { 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; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; @@ -821,6 +822,19 @@ impl Fs for RealFs { Ok(()) } + fn global_git_ignore_path(&self, abs_work_directory_path: &Path) -> Option { + new_std_command("git") + .current_dir(abs_work_directory_path) + .args(&["config", "--get", "core.excludesFile"]) + .output() + .ok() + .filter(|output| output.status.success()) + .and_then(|output| String::from_utf8(output.stdout).ok()) + .filter(|output| !output.is_empty()) + .map(|path| PathBuf::from(path)) + .or_else(|| Some(dirs::config_dir()?.join("git").join("ignore"))) + } + fn is_fake(&self) -> bool { false } @@ -2319,6 +2333,10 @@ impl Fs for FakeFs { smol::block_on(self.create_dir(&abs_work_directory_path.join(".git"))) } + fn global_git_ignore_path(&self, _abs_work_directory: &Path) -> Option { + None + } + fn is_fake(&self) -> bool { true } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index fcf466dddf..b1c19a2aeb 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3222,76 +3222,31 @@ async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { } async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { - let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); - let mut builder = GitignoreBuilder::new(parent); + + if let Some(path) = fs.global_git_ignore_path(parent) { + if let Ok(contents) = fs.load(&path).await { + for line in contents.lines() { + let _ = builder.add_line(Some(path.clone()), line); + } + } + } + + let repo_git_exclude_path = parent.join(".git").join("info").join("exclude"); + if let Ok(contents) = fs.load(&repo_git_exclude_path).await { + for line in contents.lines() { + let _ = builder.add_line(Some(repo_git_exclude_path.clone()), line); + } + } + + let contents = fs.load(abs_path).await?; for line in contents.lines() { builder.add_line(Some(abs_path.into()), line)?; } - builder.build()?; - - let mut combined_builder = GitignoreBuilder::new(parent); - - let home = home_dir(); - let repo_root = abs_path.parent().unwrap_or_else(|| Path::new("/")); - let repo_git_path = repo_root.join(".git"); - let repo_git_config_path = repo_git_path.join("config"); - - let mut global_ignore_path = None; - if let Ok(config_output) = std::process::Command::new("git") - .args(["config", "--file", &repo_git_config_path.to_string_lossy(), "core.excludesFile"]) - .output() - { - if config_output.status.success() { - let path_str = String::from_utf8_lossy(&config_output.stdout).trim().to_string(); - if !path_str.is_empty() { - let path = PathBuf::from(path_str); - global_ignore_path = Some(path); - } - } - } - - if global_ignore_path.is_none() { - if let Ok(config_output) = std::process::Command::new("git") - .args(["config", "--global", "core.excludesFile"]) - .output() - { - if config_output.status.success() { - let path_str = String::from_utf8_lossy(&config_output.stdout).trim().to_string(); - if !path_str.is_empty() { - let path = PathBuf::from(path_str); - global_ignore_path = Some(path); - } - } - } - } - - if global_ignore_path.is_none() && !home.as_os_str().is_empty() { - global_ignore_path = Some(home.join(".config/git/ignore")); - } - - let repo_git_exclude_path = repo_git_path.join("info/exclude"); - if let Ok(exclude_contents) = fs.load(&repo_git_exclude_path).await { - for line in exclude_contents.lines() { - let _ = combined_builder.add_line(Some(repo_git_exclude_path.clone()), line); - } - } - - if let Some(global_ignore_path) = global_ignore_path { - if let Ok(global_contents) = fs.load(&global_ignore_path).await { - for line in global_contents.lines() { - let _ = combined_builder.add_line(Some(global_ignore_path.clone()), line); - } - } - } - - for line in contents.lines() { - combined_builder.add_line(Some(abs_path.into()), line)?; - } - - Ok(combined_builder.build()?) + let gitignore = builder.build()?; + Ok(gitignore) } impl Deref for Worktree { From 8b0203e177006f9aefb0fbdf913cb33e6ce54534 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 28 Jun 2025 16:34:11 -0400 Subject: [PATCH 05/15] clippy --- crates/fs/src/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index da0b3a3b59..db225f628e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -831,7 +831,7 @@ impl Fs for RealFs { .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .filter(|output| !output.is_empty()) - .map(|path| PathBuf::from(path)) + .map(PathBuf::from) .or_else(|| Some(dirs::config_dir()?.join("git").join("ignore"))) } From 3119d0d06e30bd859c48c6adcfd1573157d71da4 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 17 Jul 2025 18:34:44 -0400 Subject: [PATCH 06/15] global gitignore handling in the background scanner --- crates/fs/src/fs.rs | 22 ++-- crates/project/src/project_tests.rs | 11 +- crates/util/src/paths.rs | 8 +- crates/worktree/src/ignore.rs | 12 ++ crates/worktree/src/worktree.rs | 175 +++++++++++++++++++++------- 5 files changed, 167 insertions(+), 61 deletions(-) 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) + } +} From ed685af557a0e7e2a9ec7ab1a4921cf06439ffef Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 17 Jul 2025 18:39:04 -0400 Subject: [PATCH 07/15] remove old approach --- crates/fs/src/fs.rs | 18 ------------------ crates/worktree/src/worktree.rs | 22 ++-------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6ce338310d..0cf1facfd1 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -134,7 +134,6 @@ pub trait Fs: Send + Sync { // 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; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; @@ -822,19 +821,6 @@ impl Fs for RealFs { Ok(()) } - fn global_git_ignore_path(&self, abs_work_directory_path: &Path) -> Option { - new_std_command("git") - .current_dir(abs_work_directory_path) - .args(&["config", "--get", "core.excludesFile"]) - .output() - .ok() - .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout).ok()) - .filter(|output| !output.is_empty()) - .map(PathBuf::from) - .or_else(|| Some(dirs::config_dir()?.join("git").join("ignore"))) - } - fn is_fake(&self) -> bool { false } @@ -2329,10 +2315,6 @@ impl Fs for FakeFs { smol::block_on(self.create_dir(&abs_work_directory_path.join(".git"))) } - fn global_git_ignore_path(&self, _abs_work_directory: &Path) -> Option { - None - } - fn is_fake(&self) -> bool { true } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 26d926c3c9..a0262bf604 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3233,31 +3233,13 @@ async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { } async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { + let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); let mut builder = GitignoreBuilder::new(parent); - - if let Some(path) = fs.global_git_ignore_path(parent) { - if let Ok(contents) = fs.load(&path).await { - for line in contents.lines() { - let _ = builder.add_line(Some(path.clone()), line); - } - } - } - - let repo_git_exclude_path = parent.join(".git").join("info").join("exclude"); - if let Ok(contents) = fs.load(&repo_git_exclude_path).await { - for line in contents.lines() { - let _ = builder.add_line(Some(repo_git_exclude_path.clone()), line); - } - } - - let contents = fs.load(abs_path).await?; for line in contents.lines() { builder.add_line(Some(abs_path.into()), line)?; } - - let gitignore = builder.build()?; - Ok(gitignore) + Ok(builder.build()?) } impl Deref for Worktree { From 1d990806df6201a9f6bac288ab137df03662f3eb Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 18 Jul 2025 11:59:35 -0400 Subject: [PATCH 08/15] match at repo root and add test --- crates/worktree/src/ignore.rs | 28 ++++-- crates/worktree/src/worktree.rs | 122 +++++++++++++++----------- crates/worktree/src/worktree_tests.rs | 48 +++++++++- 3 files changed, 141 insertions(+), 57 deletions(-) diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index 89c33fa280..a239520ff9 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -5,6 +5,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; pub enum IgnoreStack { None, Global { + repo_root: Option>, ignore: Arc, }, Some { @@ -24,8 +25,8 @@ impl IgnoreStack { Arc::new(Self::All) } - pub fn global(ignore: Arc) -> Arc { - Arc::new(Self::Global { ignore }) + pub fn global(repo_root: Option>, ignore: Arc) -> Arc { + Arc::new(Self::Global { repo_root, ignore }) } pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { @@ -47,11 +48,24 @@ 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::Global { repo_root, ignore } => { + let combined_path; + let abs_path = if let Some(repo_root) = repo_root { + 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, + } + } Self::Some { abs_base_path, ignore, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a0262bf604..b4a9729972 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, gitconfig_excludes_path}; +use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; 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, path, + ResultExt, paths::{PathMatcher, SanitizedPath, home_dir}, }; pub use worktree_settings::WorktreeSettings; @@ -336,23 +336,6 @@ 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, @@ -2794,8 +2777,14 @@ 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, + ) -> Arc { 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) { @@ -2804,18 +2793,17 @@ impl LocalSnapshot { new_ignores.push((ancestor, None)); } } - if ancestor.join(*DOT_GIT).exists() { - // FIXME HERE + let metadata = smol::block_on(fs.metadata(&ancestor.join(*DOT_GIT))) + .ok() + .flatten(); + if metadata.is_some() { + repo_root = Some(Arc::from(ancestor)); break; } } - // 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) + IgnoreStack::global(repo_root, global_gitignore) } else { IgnoreStack::none() }; @@ -2945,9 +2933,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) { @@ -3888,14 +3882,21 @@ impl BackgroundScanner { 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(), + ); } }; @@ -4183,7 +4184,10 @@ impl BackgroundScanner { 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); + 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(); @@ -4207,7 +4211,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; } @@ -4585,9 +4594,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(), @@ -4612,7 +4623,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; } @@ -4756,7 +4772,11 @@ impl BackgroundScanner { { ignores_to_update.next().unwrap(); } - let ignore_stack = prev_snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); + let ignore_stack = prev_snapshot.ignore_stack_for_abs_path( + &parent_abs_path, + true, + self.fs.as_ref(), + ); Some((parent_abs_path, ignore_stack)) } }); @@ -4796,7 +4816,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(), + ); } } @@ -4829,9 +4854,6 @@ impl BackgroundScanner { } } - if !entries_by_path_edits.is_empty() { - dbg!(&entries_by_path_edits); - } state .snapshot .entries_by_path @@ -5665,10 +5687,12 @@ fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, (repository_dir_abs_path, common_dir_abs_path) } +#[cfg(test)] 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) - } + Some(Path::new(util::path!("/home/zed/.config/git/ignore")).into()) +} + +#[cfg(not(test))] +fn global_gitignore_path() -> Option> { + ::ignore::gitignore::gitconfig_excludes_path().map(Into::into) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 74d5aeaf40..48eb6cc08d 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2077,7 +2077,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, _| { @@ -2098,6 +2097,53 @@ 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": "" + }, + "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(); + + worktree.update(cx, |worktree, _cx| { + check_worktree_entries(worktree, &[], &["foo", "bar"], &["sub/bar", "baz"], &[]); + }) +} + #[track_caller] fn check_worktree_entries( tree: &Worktree, From 890ff160d77bbbd84e8a1d48511052a455606d78 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 22 Jul 2025 08:47:32 -0700 Subject: [PATCH 09/15] wip --- crates/worktree/src/worktree_tests.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 48eb6cc08d..1454ad52f8 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2116,6 +2116,10 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon "foo": "", "bar": "", "sub": { + "bar": "", + }, + "subrepo": { + ".git": {}, "bar": "" }, "baz": "" @@ -2140,7 +2144,13 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon cx.run_until_parked(); worktree.update(cx, |worktree, _cx| { - check_worktree_entries(worktree, &[], &["foo", "bar"], &["sub/bar", "baz"], &[]); + check_worktree_entries( + worktree, + &[], + &["foo", "bar", "subrepo/bar"], + &["sub/bar", "baz"], + &[], + ); }) } From 37336c6211a43de06c929e5101c44308eac606b3 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 29 Jul 2025 10:43:00 -0400 Subject: [PATCH 10/15] wip --- crates/worktree/src/ignore.rs | 64 ++++++++++++++++++++++----------- crates/worktree/src/worktree.rs | 21 +++++------ 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index a239520ff9..17c362e2d7 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -1,42 +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 { - repo_root: Option>, 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 global(repo_root: Option>, ignore: Arc) -> Arc { - Arc::new(Self::Global { repo_root, ignore }) + pub fn global(ignore: Arc) -> Self { + Self { + repo_root: None, + top: Arc::new(IgnoreStackEntry::Global { ignore }), + } } - 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 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, } } @@ -45,12 +63,12 @@ impl IgnoreStack { return true; } - match self { - Self::None => false, - Self::All => true, - Self::Global { repo_root, ignore } => { + 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) = repo_root { + let abs_path = if let Some(repo_root) = self.repo_root.as_ref() { combined_path = ignore.path().join( abs_path .strip_prefix(repo_root) @@ -66,12 +84,16 @@ impl IgnoreStack { ignore::Match::Whitelist(_) => false, } } - Self::Some { + 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 b4a9729972..34ffc92f40 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2777,12 +2777,7 @@ impl LocalSnapshot { inodes } - fn ignore_stack_for_abs_path( - &self, - abs_path: &Path, - is_dir: bool, - fs: &dyn Fs, - ) -> 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() { @@ -2803,10 +2798,11 @@ impl LocalSnapshot { } let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { - IgnoreStack::global(repo_root, global_gitignore) + 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(); @@ -4369,12 +4365,14 @@ impl BackgroundScanner { swap_to_front(&mut child_paths, *GITIGNORE); swap_to_front(&mut child_paths, *DOT_GIT); + let mut repo_root = None; 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(); let child_path: Arc = job.path.join(child_name).into(); if child_name == *DOT_GIT { + repo_root = Some(child_abs_path.clone()); let mut state = self.state.lock(); state.insert_git_repository( child_path.clone(), @@ -4386,6 +4384,9 @@ impl BackgroundScanner { Ok(ignore) => { let ignore = Arc::new(ignore); ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); + if let Some(repo_root) = repo_root.clone() { + ignore_stack.repo_root = Some(repo_root); + } new_ignore = Some(ignore); } Err(error) => { @@ -4688,7 +4689,7 @@ impl BackgroundScanner { &self, scan_job_tx: Sender, prev_snapshot: LocalSnapshot, - mut ignores_to_update: impl Iterator, Arc)>, + mut ignores_to_update: impl Iterator, IgnoreStack)>, ) { let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); { @@ -5147,7 +5148,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, @@ -5155,7 +5156,7 @@ struct ScanJob { struct UpdateIgnoreStatusJob { abs_path: Arc, - ignore_stack: Arc, + ignore_stack: IgnoreStack, ignore_queue: Sender, scan_queue: Sender, } From 60a64c3e2452716d5925edac6fc66f380fe35177 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 29 Jul 2025 10:43:14 -0400 Subject: [PATCH 11/15] wip --- crates/worktree/src/worktree.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 34ffc92f40..3d59f3d6d8 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2802,9 +2802,11 @@ impl LocalSnapshot { } else { IgnoreStack::none() }; + dbg!(&abs_path, &repo_root); 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) { + dbg!("ALL"); ignore_stack = IgnoreStack::all(); break; } else if let Some(ignore) = ignore { @@ -2813,6 +2815,7 @@ impl LocalSnapshot { } if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { + dbg!("ALL"); ignore_stack = IgnoreStack::all(); } @@ -4365,14 +4368,23 @@ 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 == *DOT_GIT + { + ignore_stack.repo_root = Some(job.abs_path.clone()); + } + + // Since we check for the presence of .git first, we can register this + // directory as a repo root on the ignore stack before we call is_abs_path_ignored below. let mut repo_root = None; + dbg!("------"); for child_abs_path in child_paths { + dbg!(&child_abs_path); let child_abs_path: Arc = child_abs_path.into(); let child_name = child_abs_path.file_name().unwrap(); let child_path: Arc = job.path.join(child_name).into(); if child_name == *DOT_GIT { - repo_root = Some(child_abs_path.clone()); let mut state = self.state.lock(); state.insert_git_repository( child_path.clone(), @@ -4384,9 +4396,6 @@ impl BackgroundScanner { Ok(ignore) => { let ignore = Arc::new(ignore); ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); - if let Some(repo_root) = repo_root.clone() { - ignore_stack.repo_root = Some(repo_root); - } new_ignore = Some(ignore); } Err(error) => { @@ -4469,6 +4478,7 @@ impl BackgroundScanner { path: child_path, is_external: child_entry.is_external, ignore_stack: if child_entry.is_ignored { + dbg!("ALL"); IgnoreStack::all() } else { ignore_stack.clone() @@ -4801,13 +4811,20 @@ impl BackgroundScanner { .strip_prefix(snapshot.abs_path.as_path()) .unwrap(); + if snapshot.entry_for_path(path.join(*DOT_GIT)).is_some() { + dbg!("HERE"); + ignore_stack.repo_root = Some(job.abs_path.join(*DOT_GIT).into()); + } + for mut entry in snapshot.child_entries(path).cloned() { + dbg!(&path); let was_ignored = entry.is_ignored; let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); 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 { + dbg!("ALL"); IgnoreStack::all() } else { ignore_stack.clone() From ca458cc5290422f31b4e1a70f6a3a236cb209b7d Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 29 Jul 2025 17:22:42 -0400 Subject: [PATCH 12/15] pass down the repo root, and handle .git events --- crates/worktree/src/ignore.rs | 3 + crates/worktree/src/worktree.rs | 110 ++++++++++++++------------ crates/worktree/src/worktree_tests.rs | 108 ++++++++++++------------- 3 files changed, 119 insertions(+), 102 deletions(-) diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index 17c362e2d7..bf973e5557 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -3,6 +3,8 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; #[derive(Clone, Debug)] pub struct IgnoreStack { + /// Rooted globs (like /foo or foo/bar) in the global core.excludesFile are matched against the nearest containing repository root, + /// so we pub repo_root: Option>, pub top: Arc, } @@ -10,6 +12,7 @@ pub struct IgnoreStack { #[derive(Debug)] pub enum IgnoreStackEntry { None, + /// core.excludesFile Global { ignore: Arc, }, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 783610afd2..d6a740c3e4 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; @@ -2795,11 +2795,9 @@ impl LocalSnapshot { } else { IgnoreStack::none() }; - dbg!(&abs_path, &repo_root); 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) { - dbg!("ALL"); ignore_stack = IgnoreStack::all(); break; } else if let Some(ignore) = ignore { @@ -2808,7 +2806,6 @@ impl LocalSnapshot { } if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { - dbg!("ALL"); ignore_stack = IgnoreStack::all(); } @@ -4149,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; } { @@ -4362,17 +4368,12 @@ impl BackgroundScanner { swap_to_front(&mut child_paths, *DOT_GIT); if let Some(path) = child_paths.first() - && path == *DOT_GIT + && path.ends_with(*DOT_GIT) { ignore_stack.repo_root = Some(job.abs_path.clone()); } - // Since we check for the presence of .git first, we can register this - // directory as a repo root on the ignore stack before we call is_abs_path_ignored below. - let mut repo_root = None; - dbg!("------"); for child_abs_path in child_paths { - dbg!(&child_abs_path); let child_abs_path: Arc = child_abs_path.into(); let child_name = child_abs_path.file_name().unwrap(); let child_path: Arc = job.path.join(child_name).into(); @@ -4471,7 +4472,6 @@ impl BackgroundScanner { path: child_path, is_external: child_entry.is_external, ignore_stack: if child_entry.is_ignored { - dbg!("ALL"); IgnoreStack::all() } else { ignore_stack.clone() @@ -4737,9 +4737,10 @@ impl BackgroundScanner { .await; } - async fn update_ignore_statuses(&self, scan_job_tx: Sender) { + fn ignores_needing_update(&self) -> Vec> { 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 @@ -4760,33 +4761,31 @@ impl BackgroundScanner { } true }); + } - snapshot.clone() - }; + ignores_to_update + } - 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, - self.fs.as_ref(), - ); - Some((parent_abs_path, ignore_stack)) + 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(); } - }); - - self.update_ignore_statuses_for_paths(scan_job_tx, prev_snapshot, ignores_to_update) - .await; + 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) { @@ -4804,20 +4803,20 @@ impl BackgroundScanner { .strip_prefix(snapshot.abs_path.as_path()) .unwrap(); - if snapshot.entry_for_path(path.join(*DOT_GIT)).is_some() { - dbg!("HERE"); - ignore_stack.repo_root = Some(job.abs_path.join(*DOT_GIT).into()); + // FIXME understand the bug that causes .git to not have a snapshot entry here in the test + 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() { - dbg!(&path); let was_ignored = entry.is_ignored; let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); 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 { - dbg!("ALL"); IgnoreStack::all() } else { ignore_stack.clone() @@ -4872,10 +4871,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 @@ -4895,8 +4895,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(), @@ -4936,7 +4940,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) { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 1454ad52f8..9175803206 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1984,58 +1984,6 @@ fn test_unrelativize() { ); } -#[gpui::test] -async fn test_gitignore_with_git_info_exclude(cx: &mut TestAppContext) { - init_test(cx); - - // Test that .git/info/exclude entries are properly recognized - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".git": { - "info": { - "exclude": "excluded_file.txt\n", - }, - }, - ".gitignore": "local_ignored.txt\n", - "normal_file.txt": "normal file content", - "local_ignored.txt": "locally ignored content", - "excluded_file.txt": "excluded content", - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[], - &[ - "local_ignored.txt", // Ignored by .gitignore - "excluded_file.txt", // Ignored by .git/info/exclude - ], - &[ - "normal_file.txt", // Not ignored - ".gitignore", // Not ignored - ], - &[], - ) - }); -} - #[gpui::test] async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); @@ -2143,6 +2091,8 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon .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, @@ -2151,7 +2101,59 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon &["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"], + &[], + ); + }); + + // FIXME 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] From a9a23251b7db6311ed326275ef5e39f18cd7c25b Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 31 Jul 2025 12:46:33 -0400 Subject: [PATCH 13/15] fix tests --- Cargo.lock | 1 + crates/paths/Cargo.toml | 1 + crates/paths/src/paths.rs | 13 +++++++++++++ crates/project/src/project_tests.rs | 6 ++++-- crates/util/src/paths.rs | 2 +- crates/worktree/src/worktree.rs | 12 +----------- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0dc771b816..8e0a139fa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11475,6 +11475,7 @@ name = "paths" version = "0.1.0" dependencies = [ "dirs 4.0.0", + "ignore", "util", "workspace-hack", ] diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml index cf6dabf0e1..f6eadc5a21 100644 --- a/crates/paths/Cargo.toml +++ b/crates/paths/Cargo.toml @@ -13,5 +13,6 @@ 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..2f9e136c9d 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 4ff7234339..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, &[ diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 8007e4c696..0d310073d1 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -17,7 +17,7 @@ use crate::NumericPrefixWithSuffix; pub fn home_dir() -> &'static PathBuf { static HOME_DIR: OnceLock = OnceLock::new(); HOME_DIR.get_or_init(|| { - if cfg!(test) { + if cfg!(any(test, feature = "test-support")) { PathBuf::from("/home/zed") } else { dirs::home_dir().expect("failed to determine home directory") diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index d6a740c3e4..93bdff29bc 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3846,7 +3846,7 @@ impl BackgroundScanner { log::trace!("containing git repository: {containing_git_repository:?}"); - let global_gitignore_path = global_gitignore_path(); + 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()) @@ -5708,13 +5708,3 @@ fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, (repository_dir_abs_path, common_dir_abs_path) } - -#[cfg(test)] -fn global_gitignore_path() -> Option> { - Some(Path::new(util::path!("/home/zed/.config/git/ignore")).into()) -} - -#[cfg(not(test))] -fn global_gitignore_path() -> Option> { - ::ignore::gitignore::gitconfig_excludes_path().map(Into::into) -} From 09372534cd494ec61cc74b3a345ed249cbfb02c9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 31 Jul 2025 12:55:21 -0400 Subject: [PATCH 14/15] clippy --- Cargo.lock | 1 - crates/fs/Cargo.toml | 1 - crates/fs/src/fs.rs | 11 ----------- crates/paths/src/paths.rs | 2 +- crates/worktree/src/ignore.rs | 3 --- crates/worktree/src/worktree.rs | 2 -- crates/worktree/src/worktree_tests.rs | 3 +-- 7 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e0a139fa5..7fdc2e7fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5936,7 +5936,6 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", - "dirs 4.0.0", "fsevent", "futures 0.3.31", "git", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 3c017f607e..633fc1fc99 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -16,7 +16,6 @@ anyhow.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true -dirs.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index a407c12899..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, })), }); @@ -2360,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/src/paths.rs b/crates/paths/src/paths.rs index 2f9e136c9d..f929747ee6 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -517,6 +517,6 @@ pub fn global_gitignore_path() -> Option { pub fn global_gitignore_path() -> Option { static GLOBAL_GITIGNORE_PATH: OnceLock> = OnceLock::new(); GLOBAL_GITIGNORE_PATH - .get_or_init(|| ::ignore::gitignore::gitconfig_excludes_path()) + .get_or_init(::ignore::gitignore::gitconfig_excludes_path) .clone() } diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs index bf973e5557..17c362e2d7 100644 --- a/crates/worktree/src/ignore.rs +++ b/crates/worktree/src/ignore.rs @@ -3,8 +3,6 @@ use std::{ffi::OsStr, path::Path, sync::Arc}; #[derive(Clone, Debug)] pub struct IgnoreStack { - /// Rooted globs (like /foo or foo/bar) in the global core.excludesFile are matched against the nearest containing repository root, - /// so we pub repo_root: Option>, pub top: Arc, } @@ -12,7 +10,6 @@ pub struct IgnoreStack { #[derive(Debug)] pub enum IgnoreStackEntry { None, - /// core.excludesFile Global { ignore: Arc, }, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 93bdff29bc..1846f0909b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -4180,7 +4180,6 @@ impl BackgroundScanner { 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 @@ -4803,7 +4802,6 @@ impl BackgroundScanner { .strip_prefix(snapshot.abs_path.as_path()) .unwrap(); - // FIXME understand the bug that causes .git to not have a snapshot entry here in the test if let Ok(Some(metadata)) = smol::block_on(self.fs.metadata(&job.abs_path.join(*DOT_GIT))) && metadata.is_dir { diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 9175803206..65e0703ab2 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2127,8 +2127,7 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon ); }); - // FIXME statuses are updated when .git added/removed - + // Statuses are updated when .git added/removed fs.remove_dir( Path::new(path!("/home/zed/project/subrepo/.git")), RemoveOptions { From e2b340a00756d167f8a90bc785781c51c7cb26d0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 1 Aug 2025 10:10:45 -0400 Subject: [PATCH 15/15] fix test --- crates/paths/Cargo.toml | 3 +++ crates/worktree/Cargo.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml index f6eadc5a21..44bb0953e2 100644 --- a/crates/paths/Cargo.toml +++ b/crates/paths/Cargo.toml @@ -8,6 +8,9 @@ license = "GPL-3.0-or-later" [lints] workspace = true +[features] +test-support = [] + [lib] path = "src/paths.rs" 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"] }