git: Add support for opening git worktrees (#20164)

This adds support for [git
worktrees](https://matklad.github.io/2024/07/25/git-worktrees.html). It
fixes the errors that show up (git blame not working) and actually adds
support for detecting git changes in a `.git` folder that's outside of
our path (and not even in the ancestor chain of our root path).

(While working on this we discovered that our `.gitignore` handling is
not 100% correct. For example: we do stop processing `.gitignore` files
once we found a `.git` repository and don't go further up the ancestors,
which is correct, but then we also don't take into account the
`excludesFile` that a user might have configured, see:
https://git-scm.com/docs/gitignore)


Closes https://github.com/zed-industries/zed/issues/19842
Closes https://github.com/zed-industries/zed/issues/4670

Release Notes:

- Added support for git worktrees. Zed can now open git worktrees and
the git status in them is correctly handled.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-11-06 09:43:39 +01:00 committed by GitHub
parent 3f777f0c68
commit bd03dea296
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 337 additions and 205 deletions

View file

@ -1,3 +1,9 @@
#[cfg(target_os = "macos")]
mod mac_watcher;
#[cfg(target_os = "linux")]
pub mod linux_watcher;
use anyhow::{anyhow, Result};
use git::GitHostingProviderRegistry;
@ -530,14 +536,21 @@ impl Fs for RealFs {
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use fsevent::{EventStream, StreamFlags};
use fsevent::StreamFlags;
let (tx, rx) = smol::channel::unbounded();
let (stream, handle) = EventStream::new(&[path], latency);
std::thread::spawn(move || {
stream.run(move |events| {
smol::block_on(
tx.send(
let (events_tx, events_rx) = smol::channel::unbounded();
let handles = Arc::new(parking_lot::Mutex::new(collections::BTreeMap::default()));
let watcher = Arc::new(mac_watcher::MacWatcher::new(
events_tx,
Arc::downgrade(&handles),
latency,
));
watcher.add(path).expect("handles can't be dropped");
(
Box::pin(
events_rx
.map(|events| {
events
.into_iter()
.map(|event| {
@ -555,19 +568,14 @@ impl Fs for RealFs {
kind,
}
})
.collect(),
),
)
.is_ok()
});
});
(
Box::pin(rx.chain(futures::stream::once(async move {
drop(handle);
vec![]
}))),
Arc::new(RealWatcher {}),
.collect()
})
.chain(futures::stream::once(async move {
drop(handles);
vec![]
})),
),
watcher,
)
}
@ -580,81 +588,26 @@ impl Fs for RealFs {
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use notify::EventKind;
use parking_lot::Mutex;
let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let root_path = path.to_path_buf();
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
// Check if root path is a symlink
let target_path = self.read_link(&path).await.ok();
watcher::global({
let target_path = target_path.clone();
|g| {
let tx = tx.clone();
let pending_paths = pending_paths.clone();
g.add(move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut paths = event
.paths
.iter()
.filter_map(|path| {
if let Some(target) = target_path.clone() {
if path.starts_with(target) {
return Some(PathEvent {
path: path.clone(),
kind,
});
}
} else if path.starts_with(&root_path) {
return Some(PathEvent {
path: path.clone(),
kind,
});
}
None
})
.collect::<Vec<_>>();
if !paths.is_empty() {
paths.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
a.path.cmp(&b.path)
});
}
})
}
})
.log_err();
let watcher = Arc::new(RealWatcher {});
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
watcher.add(&path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
if let Some(parent) = path.parent() {
// watch the parent dir so we can tell when settings.json is created
watcher.add(parent).log_err();
}
// Check if path is a symlink and follow the target parent
if let Some(target) = target_path {
if let Some(target) = self.read_link(&path).await.ok() {
watcher.add(&target).ok();
if let Some(parent) = target.parent() {
watcher.add(parent).log_err();
}
}
// watch the parent dir so we can tell when settings.json is created
if let Some(parent) = path.parent() {
watcher.add(parent).log_err();
}
(
Box::pin(rx.filter_map({
let watcher = watcher.clone();
@ -784,23 +737,6 @@ impl Watcher for RealWatcher {
}
}
#[cfg(target_os = "linux")]
impl Watcher for RealWatcher {
fn add(&self, path: &Path) -> Result<()> {
use notify::Watcher;
Ok(watcher::global(|w| {
w.inotify
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??)
}
fn remove(&self, path: &Path) -> Result<()> {
use notify::Watcher;
Ok(watcher::global(|w| w.inotify.lock().unwatch(path))??)
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeFs {
// Use an unfair lock to ensure tests are deterministic.
@ -2084,49 +2020,3 @@ mod tests {
);
}
}
#[cfg(target_os = "linux")]
pub mod watcher {
use std::sync::OnceLock;
use parking_lot::Mutex;
use util::ResultExt;
pub struct GlobalWatcher {
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
pub(super) inotify: Mutex<notify::INotifyWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}
impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) {
self.watchers.lock().push(Box::new(cb))
}
}
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() {
f(&event)
}
})
.log_err();
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = INOTIFY_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
inotify: Mutex::new(file_watcher),
watchers: Default::default(),
})
});
match result {
Ok(g) => Ok(f(g)),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}
}

View file

@ -0,0 +1,121 @@
use notify::EventKind;
use parking_lot::Mutex;
use std::sync::{Arc, OnceLock};
use util::ResultExt;
use crate::{PathEvent, PathEventKind, Watcher};
pub struct LinuxWatcher {
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
}
impl LinuxWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
) -> Self {
Self {
tx,
pending_path_events,
}
}
}
impl Watcher for LinuxWatcher {
fn add(&self, path: &std::path::Path) -> gpui::Result<()> {
let root_path = path.to_path_buf();
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
use notify::Watcher;
global({
|g| {
g.add(move |event: &notify::Event| {
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
EventKind::Remove(_) => Some(PathEventKind::Removed),
_ => None,
};
let mut path_events = event
.paths
.iter()
.filter_map(|event_path| {
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.clone(),
kind,
})
})
.collect::<Vec<_>>();
if !path_events.is_empty() {
path_events.sort();
let mut pending_paths = pending_paths.lock();
if pending_paths.is_empty() {
tx.try_send(()).ok();
}
util::extend_sorted(
&mut *pending_paths,
path_events,
usize::MAX,
|a, b| a.path.cmp(&b.path),
);
}
})
}
})?;
global(|g| {
g.inotify
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??;
Ok(())
}
fn remove(&self, path: &std::path::Path) -> gpui::Result<()> {
use notify::Watcher;
Ok(global(|w| w.inotify.lock().unwatch(path))??)
}
}
pub struct GlobalWatcher {
// two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
pub(super) inotify: Mutex<notify::INotifyWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}
impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) {
self.watchers.lock().push(Box::new(cb))
}
}
static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> = OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
let Some(event) = event.log_err() else { return };
global::<()>(move |watcher| {
for f in watcher.watchers.lock().iter() {
f(&event)
}
})
.log_err();
}
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
let result = INOTIFY_INSTANCE.get_or_init(|| {
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
inotify: Mutex::new(file_watcher),
watchers: Default::default(),
})
});
match result {
Ok(g) => Ok(f(g)),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}

View file

@ -0,0 +1,70 @@
use crate::Watcher;
use anyhow::{Context as _, Result};
use collections::{BTreeMap, Bound};
use fsevent::EventStream;
use parking_lot::Mutex;
use std::{
path::{Path, PathBuf},
sync::Weak,
time::Duration,
};
pub struct MacWatcher {
events_tx: smol::channel::Sender<Vec<fsevent::Event>>,
handles: Weak<Mutex<BTreeMap<PathBuf, fsevent::Handle>>>,
latency: Duration,
}
impl MacWatcher {
pub fn new(
events_tx: smol::channel::Sender<Vec<fsevent::Event>>,
handles: Weak<Mutex<BTreeMap<PathBuf, fsevent::Handle>>>,
latency: Duration,
) -> Self {
Self {
events_tx,
handles,
latency,
}
}
}
impl Watcher for MacWatcher {
fn add(&self, path: &Path) -> Result<()> {
let handles = self
.handles
.upgrade()
.context("unable to watch path, receiver dropped")?;
let mut handles = handles.lock();
// Return early if an ancestor of this path was already being watched.
if let Some((watched_path, _)) = handles
.range::<Path, _>((Bound::Unbounded, Bound::Included(path)))
.next_back()
{
if path.starts_with(watched_path) {
return Ok(());
}
}
let (stream, handle) = EventStream::new(&[path], self.latency);
let tx = self.events_tx.clone();
std::thread::spawn(move || {
stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
});
handles.insert(path.into(), handle);
Ok(())
}
fn remove(&self, path: &Path) -> gpui::Result<()> {
let handles = self
.handles
.upgrade()
.context("unable to remove path, receiver dropped")?;
let mut handles = handles.lock();
handles.remove(path);
Ok(())
}
}

View file

@ -45,6 +45,8 @@ pub trait GitRepository: Send + Sync {
fn branch_exits(&self, _: &str) -> Result<bool>;
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
fn path(&self) -> PathBuf;
}
impl std::fmt::Debug for dyn GitRepository {
@ -83,6 +85,11 @@ impl GitRepository for RealGitRepository {
}
}
fn path(&self) -> PathBuf {
let repo = self.repository.lock();
repo.path().into()
}
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
@ -276,6 +283,11 @@ impl GitRepository for FakeGitRepository {
None
}
fn path(&self) -> PathBuf {
let state = self.state.lock();
state.path.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
let state = self.state.lock();
let mut entries = state

View file

@ -2024,6 +2024,7 @@ pub fn perform_project_search(
text: impl Into<std::sync::Arc<str>>,
cx: &mut gpui::VisualTestContext,
) {
cx.run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view
.query_editor

View file

@ -14,7 +14,6 @@ use futures::{
oneshot,
},
select_biased,
stream::select,
task::Poll,
FutureExt as _, Stream, StreamExt,
};
@ -307,9 +306,11 @@ struct BackgroundScannerState {
pub struct LocalRepositoryEntry {
pub(crate) git_dir_scan_id: usize,
pub(crate) repo_ptr: Arc<dyn GitRepository>,
/// Path to the actual .git folder.
/// Absolute path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
pub(crate) git_dir_path: Arc<Path>,
pub(crate) dot_git_dir_abs_path: Arc<Path>,
/// Absolute path to the .git file, if we're in a git worktree.
pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>,
}
impl LocalRepositoryEntry {
@ -2559,7 +2560,7 @@ impl LocalSnapshot {
new_ignores.push((ancestor, None));
}
}
if ancestor.join(*DOT_GIT).is_dir() {
if ancestor.join(*DOT_GIT).exists() {
break;
}
}
@ -2664,7 +2665,7 @@ impl LocalSnapshot {
let dotgit_paths = self
.git_repositories
.iter()
.map(|repo| repo.1.git_dir_path.clone())
.map(|repo| repo.1.dot_git_dir_abs_path.clone())
.collect::<HashSet<_>>();
let work_dir_paths = self
.repository_entries
@ -2764,11 +2765,11 @@ impl BackgroundScannerState {
}
}
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry {
self.reuse_entry_id(&mut entry);
let entry = self.snapshot.insert_entry(entry, fs);
if entry.path.file_name() == Some(&DOT_GIT) {
self.build_git_repository(entry.path.clone(), fs);
self.insert_git_repository(entry.path.clone(), fs, watcher);
}
#[cfg(test)]
@ -2897,10 +2898,11 @@ impl BackgroundScannerState {
self.snapshot.check_invariants(false);
}
fn build_git_repository(
fn insert_git_repository(
&mut self,
dot_git_path: Arc<Path>,
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<(RepositoryWorkDirectory, Arc<dyn GitRepository>)> {
let work_dir_path: Arc<Path> = match dot_git_path.parent() {
Some(parent_dir) => {
@ -2927,15 +2929,16 @@ impl BackgroundScannerState {
}
};
self.build_git_repository_for_path(work_dir_path, dot_git_path, None, fs)
self.insert_git_repository_for_path(work_dir_path, dot_git_path, None, fs, watcher)
}
fn build_git_repository_for_path(
fn insert_git_repository_for_path(
&mut self,
work_dir_path: Arc<Path>,
dot_git_path: Arc<Path>,
location_in_repo: Option<Arc<Path>>,
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<(RepositoryWorkDirectory, Arc<dyn GitRepository>)> {
let work_dir_id = self
.snapshot
@ -2946,9 +2949,31 @@ impl BackgroundScannerState {
return None;
}
let abs_path = self.snapshot.abs_path.join(&dot_git_path);
let dot_git_abs_path = self.snapshot.abs_path.join(&dot_git_path);
let t0 = Instant::now();
let repository = fs.open_repo(&abs_path)?;
let repository = fs.open_repo(&dot_git_abs_path)?;
let actual_repo_path = repository.path();
let actual_dot_git_dir_abs_path: Arc<Path> = Arc::from(
actual_repo_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))?,
);
watcher.add(&actual_repo_path).log_err()?;
let dot_git_worktree_abs_path = if actual_dot_git_dir_abs_path.as_ref() == dot_git_abs_path
{
None
} else {
// The two paths could be different because we opened a git worktree.
// When that happens, the .git path in the worktree (`dot_git_abs_path`) is a file that
// points to the worktree-subdirectory in the actual .git directory (`git_dir_path`)
watcher.add(&dot_git_abs_path).log_err()?;
Some(Arc::from(dot_git_abs_path))
};
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
@ -2972,7 +2997,8 @@ impl BackgroundScannerState {
LocalRepositoryEntry {
git_dir_scan_id: 0,
repo_ptr: repository.clone(),
git_dir_path: dot_git_path.clone(),
dot_git_dir_abs_path: actual_dot_git_dir_abs_path,
dot_git_worktree_abs_path,
},
);
@ -3542,23 +3568,27 @@ impl BackgroundScanner {
}
let ancestor_dot_git = ancestor.join(*DOT_GIT);
if ancestor_dot_git.is_dir() {
// Check whether the directory or file called `.git` exists (in the
// case of worktrees it's a file.)
if self
.fs
.metadata(&ancestor_dot_git)
.await
.is_ok_and(|metadata| metadata.is_some())
{
if index != 0 {
// We canonicalize, since the FS events use the canonicalized path.
if let Some(ancestor_dot_git) =
self.fs.canonicalize(&ancestor_dot_git).await.log_err()
{
let (ancestor_git_events, _) =
self.fs.watch(&ancestor_dot_git, FS_WATCH_LATENCY).await;
fs_events_rx = select(fs_events_rx, ancestor_git_events).boxed();
// We associate the external git repo with our root folder and
// also mark where in the git repo the root folder is located.
self.state.lock().build_git_repository_for_path(
self.state.lock().insert_git_repository_for_path(
Path::new("").into(),
ancestor_dot_git.into(),
Some(root_abs_path.strip_prefix(ancestor).unwrap().into()),
self.fs.as_ref(),
self.watcher.as_ref(),
);
};
}
@ -3578,7 +3608,7 @@ impl BackgroundScanner {
.ignore_stack_for_abs_path(&root_abs_path, true);
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref());
state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
}
state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
}
@ -3708,7 +3738,7 @@ impl BackgroundScanner {
};
let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut dot_git_paths = Vec::new();
let mut dot_git_abs_paths = Vec::new();
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(b));
abs_paths.retain(|abs_path| {
@ -3723,7 +3753,7 @@ impl BackgroundScanner {
FsMonitor
}
let mut fsmonitor_parse_state = None;
if let Some(dot_git_dir) = abs_path
if let Some(dot_git_abs_path) = abs_path
.ancestors()
.find(|ancestor| {
let file_name = ancestor.file_name();
@ -3742,12 +3772,9 @@ impl BackgroundScanner {
})
{
let dot_git_path = dot_git_dir
.strip_prefix(&root_canonical_path)
.unwrap_or(dot_git_dir)
.to_path_buf();
if !dot_git_paths.contains(&dot_git_path) {
dot_git_paths.push(dot_git_path);
let dot_git_abs_path = dot_git_abs_path.to_path_buf();
if !dot_git_abs_paths.contains(&dot_git_abs_path) {
dot_git_abs_paths.push(dot_git_abs_path);
}
is_git_related = true;
}
@ -3790,7 +3817,7 @@ impl BackgroundScanner {
}
});
if relative_paths.is_empty() && dot_git_paths.is_empty() {
if relative_paths.is_empty() && dot_git_abs_paths.is_empty() {
return;
}
@ -3810,8 +3837,8 @@ impl BackgroundScanner {
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
if !dot_git_paths.is_empty() {
self.update_git_repositories(dot_git_paths).await;
if !dot_git_abs_paths.is_empty() {
self.update_git_repositories(dot_git_abs_paths).await;
}
{
@ -3995,10 +4022,12 @@ impl BackgroundScanner {
let child_path: Arc<Path> = job.path.join(child_name).into();
if child_name == *DOT_GIT {
let repo = self
.state
.lock()
.build_git_repository(child_path.clone(), self.fs.as_ref());
let repo = self.state.lock().insert_git_repository(
child_path.clone(),
self.fs.as_ref(),
self.watcher.as_ref(),
);
if let Some((work_directory, repository)) = repo {
let t0 = Instant::now();
let statuses = repository
@ -4011,7 +4040,6 @@ impl BackgroundScanner {
statuses,
});
}
self.watcher.add(child_abs_path.as_ref()).log_err();
} else if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
Ok(ignore) => {
@ -4221,7 +4249,7 @@ impl BackgroundScanner {
if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(relative_path) {
if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, relative_path) {
paths_by_git_repo
.entry(repo.git_dir_path.clone())
.entry(repo.dot_git_dir_abs_path.clone())
.or_insert_with(|| RepoPaths {
repo: repo.repo_ptr.clone(),
repo_paths: Vec::new(),
@ -4281,7 +4309,7 @@ impl BackgroundScanner {
fs_entry.git_status = git_statuses_by_relative_path.remove(path);
}
state.insert_entry(fs_entry.clone(), self.fs.as_ref());
state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
}
Ok(None) => {
self.remove_repo_path(path, &mut state.snapshot);
@ -4494,13 +4522,22 @@ impl BackgroundScanner {
.git_repositories
.iter()
.find_map(|(entry_id, repo)| {
(repo.git_dir_path.as_ref() == dot_git_dir)
.then(|| (*entry_id, repo.clone()))
if repo.dot_git_dir_abs_path.as_ref() == &dot_git_dir
|| repo.dot_git_worktree_abs_path.as_deref() == Some(&dot_git_dir)
{
Some((*entry_id, repo.clone()))
} else {
None
}
});
let (work_directory, repository) = match existing_repository_entry {
None => {
match state.build_git_repository(dot_git_dir.into(), self.fs.as_ref()) {
match state.insert_git_repository(
dot_git_dir.into(),
self.fs.as_ref(),
self.watcher.as_ref(),
) {
Some(output) => output,
None => continue,
}
@ -4555,19 +4592,14 @@ impl BackgroundScanner {
.map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
});
if exists_in_snapshot {
if exists_in_snapshot
|| matches!(
smol::block_on(self.fs.metadata(&entry.dot_git_dir_abs_path)),
Ok(Some(_))
)
{
ids_to_preserve.insert(work_directory_id);
} else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
let git_dir_excluded = self.settings.is_path_excluded(&entry.git_dir_path);
if git_dir_excluded
&& !matches!(
smol::block_on(self.fs.metadata(&git_dir_abs_path)),
Ok(None)
)
{
ids_to_preserve.insert(work_directory_id);
}
}
}
@ -4960,7 +4992,7 @@ impl WorktreeModelHandle for Model<Worktree> {
let local_repo_entry = tree.get_local_repo(&root_entry).unwrap();
(
tree.fs.clone(),
local_repo_entry.git_dir_path.clone(),
local_repo_entry.dot_git_dir_abs_path.clone(),
local_repo_entry.git_dir_scan_id,
)
});

View file

@ -720,7 +720,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, true);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
});
@ -757,7 +757,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
Some(GitFileStatus::Added),
false,
);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, true);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
});
@ -843,7 +843,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
.unwrap();
#[cfg(target_os = "linux")]
fs::watcher::global(|_| {}).unwrap();
fs::linux_watcher::global(|_| {}).unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
@ -2635,6 +2635,12 @@ fn assert_entry_git_state(
is_ignored: bool,
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
assert_eq!(entry.git_status, git_status);
assert_eq!(entry.is_ignored, is_ignored);
assert_eq!(
entry.git_status, git_status,
"expected {path} to have git status: {git_status:?}"
);
assert_eq!(
entry.is_ignored, is_ignored,
"expected {path} to have is_ignored: {is_ignored}"
);
}

View file

@ -154,7 +154,7 @@ pub fn initialize_workspace(
.detach();
#[cfg(target_os = "linux")]
if let Err(e) = fs::watcher::global(|_| {}) {
if let Err(e) = fs::linux_watcher::global(|_| {}) {
let message = format!(db::indoc!{r#"
inotify_init returned {}