From dc3158c8ce1385fe8c71ff83aba9082387f482fa Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 3 Mar 2025 15:33:02 -0500 Subject: [PATCH] git: Don't consider $HOME as containing git repository unless it's opened directly (#25948) When a worktree is created, we walk up the ancestors of the root path trying to find a git repository. In particular, if your `$HOME` is a git repository and you open some subdirectory of `$HOME` that's *not* a git repository, we end up scanning `$HOME` and everything under it looking for changed and untracked files, which is often pretty slow. Consistency here is not very useful and leads to a bad experience. This PR adds a special case to not consider `$HOME` as a containing git repository, unless you ask for it by doing the equivalent of `zed ~`. Release Notes: - Changed the behavior of git features to not treat `$HOME` as a git repository unless opened directly --- crates/fs/src/fs.rs | 15 ++++++ crates/worktree/src/worktree.rs | 6 ++- crates/worktree/src/worktree_tests.rs | 67 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9af1d92dea67f82913283691f64c37f966a0adae..b38521756ab6f1a3511eeba1c183777c6d20c88b 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -135,6 +135,7 @@ pub trait Fs: Send + Sync { Arc, ); + fn home_dir(&self) -> Option; fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; @@ -813,6 +814,10 @@ 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")))] @@ -846,6 +851,7 @@ struct FakeFsState { metadata_call_count: usize, read_dir_call_count: usize, moves: std::collections::HashMap, + home_dir: Option, } #[cfg(any(test, feature = "test-support"))] @@ -1031,6 +1037,7 @@ impl FakeFs { read_dir_call_count: 0, metadata_call_count: 0, moves: Default::default(), + home_dir: None, }), }); @@ -1524,6 +1531,10 @@ 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"))] @@ -2079,6 +2090,10 @@ 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/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0d2ec99e9c89560e4f9e7989692a30bdb9deb97b..647d7830755a5fef74b0c88ff0717c5e48fee934 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -4292,7 +4292,11 @@ impl BackgroundScanner { let mut containing_git_repository = None; for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { if index != 0 { - if let Ok(ignore) = + if Some(ancestor) == self.fs.home_dir().as_deref() { + // Unless $HOME is itself the worktree root, don't consider it as a + // containing git repository---expensive and likely unwanted. + break; + } else if let Ok(ignore) = build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await { self.state diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index cd2c2e051d5b69887937f1a85cfc86fe501a27c3..d889cc71cb15add059e3572c7253c9a3a4e604d8 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2241,6 +2241,73 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_home_dir_as_git_repository(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "home": { + ".git": {}, + "project": { + "a.txt": "A" + }, + }, + }), + ) + .await; + fs.set_home_dir(Path::new(path!("/root/home")).to_owned()); + + let tree = Worktree::local( + Path::new(path!("/root/home/project")), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + let repo = tree.repository_for_path(path!("a.txt").as_ref()); + assert!(repo.is_none()); + }); + + let home_tree = Worktree::local( + Path::new(path!("/root/home")), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| home_tree.read(cx).as_local().unwrap().scan_complete()) + .await; + home_tree.flush_fs_events(cx).await; + + home_tree.read_with(cx, |home_tree, _cx| { + let home_tree = home_tree.as_local().unwrap(); + + let repo = home_tree.repository_for_path(path!("project/a.txt").as_ref()); + assert_eq!( + repo.map(|repo| &repo.work_directory), + Some(&WorkDirectory::InProject { + relative_path: Path::new("").into() + }) + ); + }) +} + #[gpui::test] async fn test_git_repository_for_path(cx: &mut TestAppContext) { init_test(cx);