global gitignore handling in the background scanner

This commit is contained in:
Cole Miller 2025-07-17 18:34:44 -04:00
parent 8b0203e177
commit 3119d0d06e
5 changed files with 167 additions and 61 deletions

View file

@ -4,6 +4,9 @@ use std::{ffi::OsStr, path::Path, sync::Arc};
#[derive(Debug)]
pub enum IgnoreStack {
None,
Global {
ignore: Arc<Gitignore>,
},
Some {
abs_base_path: Arc<Path>,
ignore: Arc<Gitignore>,
@ -21,6 +24,10 @@ impl IgnoreStack {
Arc::new(Self::All)
}
pub fn global(ignore: Arc<Gitignore>) -> Arc<Self> {
Arc::new(Self::Global { ignore })
}
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
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,

View file

@ -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<ProjectEntryId> for WorkDirectoryEntry {
#[derive(Debug, Clone)]
pub struct LocalSnapshot {
snapshot: Snapshot,
global_gitignore: Option<Arc<Gitignore>>,
/// 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<Path>, (Arc<Gitignore>, bool)>,
@ -510,6 +511,7 @@ impl Worktree {
cx.new(move |cx: &mut Context<Worktree>| {
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<Path>]) -> 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<ScanJob>) {
let mut ignores_to_update = Vec::new();
async fn update_ignore_statuses_for_paths(
&self,
scan_job_tx: Sender<ScanJob>,
prev_snapshot: LocalSnapshot,
mut ignores_to_update: impl Iterator<Item = (Arc<Path>, Arc<IgnoreStack>)>,
) {
let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
let prev_snapshot;
{
let snapshot = &mut self.state.lock().snapshot;
let abs_path = snapshot.abs_path.clone();
snapshot
.ignores_by_parent_abs_path
.retain(|parent_abs_path, (_, needs_update)| {
if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) {
if *needs_update {
*needs_update = false;
if snapshot.snapshot.entry_for_path(parent_path).is_some() {
ignores_to_update.push(parent_abs_path.clone());
}
}
let ignore_path = parent_path.join(*GITIGNORE);
if snapshot.snapshot.entry_for_path(ignore_path).is_none() {
return false;
}
}
true
});
ignores_to_update.sort_unstable();
let mut ignores_to_update = ignores_to_update.into_iter().peekable();
while let Some(parent_abs_path) = ignores_to_update.next() {
while ignores_to_update
.peek()
.map_or(false, |p| p.starts_with(&parent_abs_path))
{
ignores_to_update.next().unwrap();
}
let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
while let Some((parent_abs_path, ignore_stack)) = ignores_to_update.next() {
ignore_queue_tx
.send_blocking(UpdateIgnoreStatusJob {
abs_path: parent_abs_path,
@ -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<ScanJob>) {
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<Path>, fs: &dyn Fs) -> (Arc<Path>,
(repository_dir_abs_path, common_dir_abs_path)
}
fn global_gitignore_path() -> Option<Arc<Path>> {
if cfg!(test) {
Some(Path::new(path!("/home/zed/.config/git/ignore")).into())
} else {
gitconfig_excludes_path().map(Into::into)
}
}