pass down the repo root, and handle .git events

This commit is contained in:
Cole Miller 2025-07-29 17:22:42 -04:00
parent 17822762b7
commit ca458cc529
3 changed files with 119 additions and 102 deletions

View file

@ -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<Arc<Path>>,
pub top: Arc<IgnoreStackEntry>,
}
@ -10,6 +12,7 @@ pub struct IgnoreStack {
#[derive(Debug)]
pub enum IgnoreStackEntry {
None,
/// core.excludesFile
Global {
ignore: Arc<Gitignore>,
},

View file

@ -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<Path> = child_abs_path.into();
let child_name = child_abs_path.file_name().unwrap();
let child_path: Arc<Path> = 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<ScanJob>) {
fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
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<Arc<Path>>,
) -> impl use<> + Iterator<Item = (Arc<Path>, 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<Path> = 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<PathBuf>) {
fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Vec<Arc<Path>> {
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) {

View file

@ -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]