From 05b161118c753e72fa61710b85c91f07f8e3c626 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:05:42 -0700 Subject: [PATCH 1/7] Don't call git status when ignored files change --- crates/fs/src/repository.rs | 1 + crates/project/src/worktree.rs | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 611427c0a8..0a43c7ee26 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -109,6 +109,7 @@ impl GitRepository for LibGitRepository { } } } + fn branches(&self) -> Result> { let local_branches = self.branches(Some(BranchType::Local))?; let valid_branches = local_branches diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2ce1693459..880313b6b1 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2162,10 +2162,12 @@ impl BackgroundScannerState { let path = entry.path.clone(); let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); - let containing_repository = self - .snapshot - .local_repo_for_path(&path) - .map(|(path, repo)| (path, repo.repo_ptr.lock().statuses())); + let mut containing_repository = None; + if !ignore_stack.is_all() { + if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { + containing_repository = Some((workdir_path, repo.repo_ptr.lock().statuses())); + } + } if !ancestor_inodes.contains(&entry.inode) { ancestor_inodes.insert(entry.inode); scan_job_tx @@ -3517,10 +3519,12 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - - if let Some((repository_dir, statuses)) = &job.containing_repository { - if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { - child_entry.git_status = statuses.get(&RepoPath(repo_path.into())).copied(); + if !child_entry.is_ignored { + if let Some((repository_dir, statuses)) = &job.containing_repository { + if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { + let repo_path = RepoPath(repo_path.into()); + child_entry.git_status = statuses.get(&repo_path).copied(); + } } } } From ff0864026ed9c981ed15252da4d4541e2190bfd9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:08:31 -0700 Subject: [PATCH 2/7] Only fetch statuses for changed paths --- crates/fs/src/repository.rs | 14 +++++++++----- crates/project/src/worktree.rs | 9 ++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 0a43c7ee26..851d495b01 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -27,7 +27,7 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; - fn statuses(&self) -> TreeMap; + fn statuses(&self, path_prefix: &Path) -> TreeMap; fn status(&self, path: &RepoPath) -> Result>; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -78,9 +78,11 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> TreeMap { + fn statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); - if let Some(statuses) = self.statuses(None).log_err() { + let mut options = git2::StatusOptions::new(); + options.pathspec(path_prefix); + if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() .filter(|status| !status.status().contains(git2::Status::IGNORED)) @@ -201,11 +203,13 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> TreeMap { + fn statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let state = self.state.lock(); for (repo_path, status) in state.worktree_statuses.iter() { - map.insert(repo_path.to_owned(), status.to_owned()); + if repo_path.0.starts_with(path_prefix) { + map.insert(repo_path.to_owned(), status.to_owned()); + } } map } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 880313b6b1..85cf032464 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2165,7 +2165,10 @@ impl BackgroundScannerState { let mut containing_repository = None; if !ignore_stack.is_all() { if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { - containing_repository = Some((workdir_path, repo.repo_ptr.lock().statuses())); + if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { + containing_repository = + Some((workdir_path, repo.repo_ptr.lock().statuses(repo_path))); + } } } if !ancestor_inodes.contains(&entry.inode) { @@ -2357,7 +2360,7 @@ impl BackgroundScannerState { .repository_entries .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - let statuses = repository.statuses(); + let statuses = repository.statuses(Path::new("")); self.update_git_statuses(&work_dir, &statuses); } } @@ -2415,7 +2418,7 @@ impl BackgroundScannerState { }, ); - let statuses = repo_lock.statuses(); + let statuses = repo_lock.statuses(Path::new("")); self.update_git_statuses(&work_directory, &statuses); drop(repo_lock); From 51d311affd108118f03aef42f2ad4937bfb334fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:51:00 -0700 Subject: [PATCH 3/7] Compute unstaged git status separately, to take advantage of our cached file mtimes --- crates/fs/src/repository.rs | 34 ++++++++++++-- crates/project/src/worktree.rs | 83 +++++++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 851d495b01..54f80c26a2 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -10,6 +10,7 @@ use std::{ os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, sync::Arc, + time::SystemTime, }; use sum_tree::{MapSeekTarget, TreeMap}; use util::ResultExt; @@ -27,7 +28,8 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; - fn statuses(&self, path_prefix: &Path) -> TreeMap; + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap; + fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option; fn status(&self, path: &RepoPath) -> Result>; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -78,10 +80,11 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self, path_prefix: &Path) -> TreeMap { + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); + options.disable_pathspec_match(true); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() @@ -98,6 +101,27 @@ impl GitRepository for LibGitRepository { map } + fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option { + let index = self.index().log_err()?; + if let Some(entry) = index.get_path(&path, 0) { + let mtime = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err()?; + if entry.mtime.seconds() == mtime.as_secs() as i32 + && entry.mtime.nanoseconds() == mtime.subsec_nanos() + { + None + } else { + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status + } + } else { + Some(GitFileStatus::Added) + } + } + fn status(&self, path: &RepoPath) -> Result> { let status = self.status_file(path); match status { @@ -203,7 +227,7 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self, path_prefix: &Path) -> TreeMap { + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let state = self.state.lock(); for (repo_path, status) in state.worktree_statuses.iter() { @@ -214,6 +238,10 @@ impl GitRepository for FakeGitRepository { map } + fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option { + None + } + fn status(&self, path: &RepoPath) -> Result> { let state = self.state.lock(); Ok(state.worktree_statuses.get(path).cloned()) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 85cf032464..c4b6ed6ca0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2166,8 +2166,11 @@ impl BackgroundScannerState { if !ignore_stack.is_all() { if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { - containing_repository = - Some((workdir_path, repo.repo_ptr.lock().statuses(repo_path))); + containing_repository = Some(( + workdir_path, + repo.repo_ptr.clone(), + repo.repo_ptr.lock().staged_statuses(repo_path), + )); } } } @@ -2360,8 +2363,7 @@ impl BackgroundScannerState { .repository_entries .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - let statuses = repository.statuses(Path::new("")); - self.update_git_statuses(&work_dir, &statuses); + self.update_git_statuses(&work_dir, &*repository); } } } @@ -2386,7 +2388,11 @@ impl BackgroundScannerState { &mut self, dot_git_path: Arc, fs: &dyn Fs, - ) -> Option<(RepositoryWorkDirectory, TreeMap)> { + ) -> Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )> { log::info!("build git repository {:?}", dot_git_path); let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); @@ -2418,27 +2424,28 @@ impl BackgroundScannerState { }, ); - let statuses = repo_lock.statuses(Path::new("")); - self.update_git_statuses(&work_directory, &statuses); + let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock); drop(repo_lock); self.snapshot.git_repositories.insert( work_dir_id, LocalRepositoryEntry { git_dir_scan_id: 0, - repo_ptr: repository, + repo_ptr: repository.clone(), git_dir_path: dot_git_path.clone(), }, ); - Some((work_directory, statuses)) + Some((work_directory, repository, staged_statuses)) } fn update_git_statuses( &mut self, work_directory: &RepositoryWorkDirectory, - statuses: &TreeMap, - ) { + repo: &dyn GitRepository, + ) -> TreeMap { + let staged_statuses = repo.staged_statuses(Path::new("")); + let mut changes = vec![]; let mut edits = vec![]; @@ -2451,7 +2458,10 @@ impl BackgroundScannerState { continue; }; let repo_path = RepoPath(repo_path.to_path_buf()); - let git_file_status = statuses.get(&repo_path).copied(); + let git_file_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repo.unstaged_status(&repo_path, entry.mtime), + ); if entry.git_status != git_file_status { entry.git_status = git_file_status; changes.push(entry.path.clone()); @@ -2461,6 +2471,7 @@ impl BackgroundScannerState { self.snapshot.entries_by_path.edit(edits, &()); util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp); + staged_statuses } } @@ -3523,10 +3534,17 @@ impl BackgroundScanner { } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); if !child_entry.is_ignored { - if let Some((repository_dir, statuses)) = &job.containing_repository { + if let Some((repository_dir, repository, staged_statuses)) = + &job.containing_repository + { if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { let repo_path = RepoPath(repo_path.into()); - child_entry.git_status = statuses.get(&repo_path).copied(); + child_entry.git_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repository + .lock() + .unstaged_status(&repo_path, child_entry.mtime), + ); } } } @@ -3637,13 +3655,11 @@ impl BackgroundScanner { if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) { - if let Ok(path) = path.strip_prefix(work_dir.0) { - fs_entry.git_status = repo - .repo_ptr - .lock() - .status(&RepoPath(path.into())) - .log_err() - .flatten() + if let Ok(repo_path) = path.strip_prefix(work_dir.0) { + let repo_path = RepoPath(repo_path.into()); + let repo = repo.repo_ptr.lock(); + fs_entry.git_status = + repo.status(&repo_path).log_err().flatten(); } } } @@ -3997,7 +4013,11 @@ struct ScanJob { scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, - containing_repository: Option<(RepositoryWorkDirectory, TreeMap)>, + containing_repository: Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )>, } struct UpdateIgnoreStatusJob { @@ -4324,3 +4344,22 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } } + +fn combine_git_statuses( + staged: Option, + unstaged: Option, +) -> Option { + if let Some(staged) = staged { + if let Some(unstaged) = unstaged { + if unstaged != staged { + Some(GitFileStatus::Modified) + } else { + Some(staged) + } + } else { + Some(staged) + } + } else { + unstaged + } +} From 6c09782aa2b132216a55d8028721eebe17786151 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 11:53:26 -0700 Subject: [PATCH 4/7] Optimize full file status via passing in known file mtime --- crates/fs/src/repository.rs | 65 +++++++++++++++++++++++++--------- crates/project/src/worktree.rs | 3 +- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 54f80c26a2..e6a41839b6 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,6 @@ use anyhow::Result; use collections::HashMap; -use git2::{BranchType, ErrorCode}; +use git2::{BranchType, StatusShow}; use parking_lot::Mutex; use rpc::proto; use serde_derive::{Deserialize, Serialize}; @@ -28,9 +28,25 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; + + /// Get the statuses of all of the files in the index that start with the given + /// path and have changes with resepect to the HEAD commit. This is fast because + /// the index stores hashes of trees, so that unchanged directories can be skipped. fn staged_statuses(&self, path_prefix: &Path) -> TreeMap; + + /// Get the status of a given file in the working directory with respect to + /// the index. In the common case, when there are no changes, this only requires + /// an index lookup. The index stores the mtime of each file when it was added, + /// so there's no work to do if the mtime matches. fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option; - fn status(&self, path: &RepoPath) -> Result>; + + /// Get the status of a given file in the working directory with respect to + /// the HEAD commit. In the common case, when there are no changes, this only + /// requires an index lookup and blob comparison between the index and the HEAD + /// commit. The index stores the mtime of each file when it was added, so there's + /// no need to consider the working directory file if the mtime matches. + fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option; + fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; fn create_branch(&self, _: &str) -> Result<()>; @@ -42,7 +58,6 @@ impl std::fmt::Debug for dyn GitRepository { } } -#[async_trait::async_trait] impl GitRepository for LibGitRepository { fn reload_index(&self) { if let Ok(mut index) = self.index() { @@ -122,18 +137,21 @@ impl GitRepository for LibGitRepository { } } - fn status(&self, path: &RepoPath) -> Result> { - let status = self.status_file(path); - match status { - Ok(status) => Ok(read_status(status)), - Err(e) => { - if e.code() == ErrorCode::NotFound { - Ok(None) - } else { - Err(e.into()) - } - } + fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option { + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + + // If the file has not changed since it was added to the index, then + // there's no need to examine the working directory file: just compare + // the blob in the index to the one in the HEAD commit. + if matches_index(self, path, mtime) { + options.show(StatusShow::Index); } + + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status } fn branches(&self) -> Result> { @@ -178,6 +196,21 @@ impl GitRepository for LibGitRepository { } } +fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool { + if let Some(index) = repo.index().log_err() { + if let Some(entry) = index.get_path(&path, 0) { + if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() { + if entry.mtime.seconds() == mtime.as_secs() as i32 + && entry.mtime.nanoseconds() == mtime.subsec_nanos() + { + return true; + } + } + } + } + false +} + fn read_status(status: git2::Status) -> Option { if status.contains(git2::Status::CONFLICTED) { Some(GitFileStatus::Conflict) @@ -242,9 +275,9 @@ impl GitRepository for FakeGitRepository { None } - fn status(&self, path: &RepoPath) -> Result> { + fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option { let state = self.state.lock(); - Ok(state.worktree_statuses.get(path).cloned()) + state.worktree_statuses.get(path).cloned() } fn branches(&self) -> Result> { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c4b6ed6ca0..b0795818b8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3658,8 +3658,7 @@ impl BackgroundScanner { if let Ok(repo_path) = path.strip_prefix(work_dir.0) { let repo_path = RepoPath(repo_path.into()); let repo = repo.repo_ptr.lock(); - fs_entry.git_status = - repo.status(&repo_path).log_err().flatten(); + fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime); } } } From b338ffe8d818411d0c4a824d7e3f5f92711794da Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 17:47:36 -0700 Subject: [PATCH 5/7] Rely on git status for any paths not matching the git index --- crates/fs/src/repository.rs | 38 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index e6a41839b6..47e1bc1aab 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -97,9 +97,11 @@ impl GitRepository for LibGitRepository { fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); + let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); options.disable_pathspec_match(true); + if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() @@ -117,30 +119,32 @@ impl GitRepository for LibGitRepository { } fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option { - let index = self.index().log_err()?; - if let Some(entry) = index.get_path(&path, 0) { - let mtime = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err()?; - if entry.mtime.seconds() == mtime.as_secs() as i32 - && entry.mtime.nanoseconds() == mtime.subsec_nanos() - { - None - } else { - let mut options = git2::StatusOptions::new(); - options.pathspec(&path.0); - options.disable_pathspec_match(true); - let statuses = self.statuses(Some(&mut options)).log_err()?; - let status = statuses.get(0).and_then(|s| read_status(s.status())); - status - } - } else { - Some(GitFileStatus::Added) + // If the file has not changed since it was added to the index, then + // there can't be any changes. + if matches_index(self, path, mtime) { + return None; } + + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + options.include_untracked(true); + options.recurse_untracked_dirs(true); + options.include_unmodified(true); + options.show(StatusShow::Workdir); + + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status } fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option { let mut options = git2::StatusOptions::new(); options.pathspec(&path.0); options.disable_pathspec_match(true); + options.include_untracked(true); + options.recurse_untracked_dirs(true); + options.include_unmodified(true); // If the file has not changed since it was added to the index, then // there's no need to examine the working directory file: just compare From a3a9d024ba3323ae4b67e2dea4ecfb88ce6a489b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 17:53:58 -0700 Subject: [PATCH 6/7] Fix filtering of staged statuses --- crates/fs/src/repository.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 47e1bc1aab..f4678d933f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -101,18 +101,17 @@ impl GitRepository for LibGitRepository { let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); options.disable_pathspec_match(true); + options.show(StatusShow::Index); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { - for status in statuses - .iter() - .filter(|status| !status.status().contains(git2::Status::IGNORED)) - { + for status in statuses.iter() { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - let Some(status) = read_status(status.status()) else { - continue - }; - - map.insert(path, status) + let status = status.status(); + if !status.contains(git2::Status::IGNORED) { + if let Some(status) = read_status(status) { + map.insert(path, status) + } + } } } map From 8fff0b0ff8ade2b43f2608824a3cc7b3878afae3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 23 Jul 2023 21:34:12 -0700 Subject: [PATCH 7/7] Fix pathspec in staged_statuses Enable non-literal matching so that directory paths match all files contained within them. --- crates/fs/src/repository.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index f4678d933f..2b2aebe679 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -100,7 +100,6 @@ impl GitRepository for LibGitRepository { let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); - options.disable_pathspec_match(true); options.show(StatusShow::Index); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {