Detailed changes
@@ -11598,6 +11598,7 @@ name = "paths"
version = "0.1.0"
dependencies = [
"dirs 4.0.0",
+ "ignore",
"util",
"workspace-hack",
]
@@ -16437,7 +16438,6 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
- "dirs 4.0.0",
"futures 0.3.31",
"gpui",
"libc",
@@ -428,7 +428,9 @@ mod tests {
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
+ use serde_json::json;
use settings::SettingsStore;
+ use util::test::TempTree;
fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok();
@@ -449,6 +451,8 @@ mod tests {
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
+ let tree = TempTree::new(json!({}));
+ util::paths::set_home_dir(tree.path().into());
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
@@ -19,7 +19,7 @@ use std::sync::LazyLock;
use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{AssistantEventData, AssistantPhase, Event, EventRequestBody, EventWrapper};
-use util::{ResultExt, TryFutureExt};
+use util::TryFutureExt;
use worktree::{UpdatedEntriesSet, WorktreeId};
use self::event_coalescer::EventCoalescer;
@@ -209,7 +209,7 @@ impl Telemetry {
let os_version = os_version();
state.lock().os_version = Some(os_version);
async move {
- if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
+ if let Some(tempfile) = File::create(Self::log_file_path()).ok() {
state.lock().log_file = Some(tempfile);
}
}
@@ -134,7 +134,6 @@ pub trait Fs: Send + Sync {
Arc<dyn Watcher>,
);
- fn home_dir(&self) -> Option<PathBuf>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>;
@@ -916,10 +915,6 @@ impl Fs for RealFs {
temp_dir.close()?;
case_sensitive
}
-
- fn home_dir(&self) -> Option<PathBuf> {
- Some(paths::home_dir().clone())
- }
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
@@ -954,7 +949,6 @@ struct FakeFsState {
read_dir_call_count: usize,
path_write_counts: std::collections::HashMap<PathBuf, usize>,
moves: std::collections::HashMap<u64, PathBuf>,
- home_dir: Option<PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -1239,7 +1233,6 @@ impl FakeFs {
metadata_call_count: 0,
path_write_counts: Default::default(),
moves: Default::default(),
- home_dir: None,
})),
});
@@ -1902,10 +1895,6 @@ impl FakeFs {
fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
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"))]
@@ -2499,10 +2488,6 @@ impl Fs for FakeFs {
fn as_fake(&self) -> Arc<FakeFs> {
self.this.upgrade().unwrap()
}
-
- fn home_dir(&self) -> Option<PathBuf> {
- self.state.lock().home_dir.clone()
- }
}
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
@@ -8,10 +8,14 @@ license = "GPL-3.0-or-later"
[lints]
workspace = true
+[features]
+test-support = []
+
[lib]
path = "src/paths.rs"
[dependencies]
dirs.workspace = true
+ignore.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -520,3 +520,16 @@ fn add_vscode_user_data_paths(paths: &mut Vec<PathBuf>, product_name: &str) {
);
}
}
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn global_gitignore_path() -> Option<PathBuf> {
+ Some(home_dir().join(".config").join("git").join("ignore"))
+}
+
+#[cfg(not(any(test, feature = "test-support")))]
+pub fn global_gitignore_path() -> Option<PathBuf> {
+ static GLOBAL_GITIGNORE_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
+ GLOBAL_GITIGNORE_PATH
+ .get_or_init(::ignore::gitignore::gitconfig_excludes_path)
+ .clone()
+}
@@ -31,7 +31,7 @@ use lsp::{
Uri, WillRenameFiles, notification::DidRenameFiles,
};
use parking_lot::Mutex;
-use paths::{config_dir, tasks_file};
+use paths::{config_dir, global_gitignore_path, tasks_file};
use postage::stream::Stream as _;
use pretty_assertions::{assert_eq, assert_matches};
use rand::{Rng as _, rngs::StdRng};
@@ -1391,7 +1391,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5);
let mut new_watched_paths = fs.watched_paths();
- new_watched_paths.retain(|path| !path.starts_with(config_dir()));
+ new_watched_paths.retain(|path| {
+ !path.starts_with(config_dir()) && !path.starts_with(global_gitignore_path().unwrap())
+ });
assert_eq!(
&new_watched_paths,
&[
@@ -7942,21 +7944,19 @@ async fn test_repository_and_path_for_project_path(
async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
+ let home = paths::home_dir();
fs.insert_tree(
- path!("/root"),
+ home,
json!({
- "home": {
- ".git": {},
- "project": {
- "a.txt": "A"
- },
+ ".git": {},
+ "project": {
+ "a.txt": "A"
},
}),
)
.await;
- fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
- let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
+ let project = Project::test(fs.clone(), [home.join("project").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
@@ -7973,7 +7973,7 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
assert!(containing.is_none());
});
- let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
+ let project = Project::test(fs.clone(), [home.as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
project
@@ -7993,7 +7993,7 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
.read(cx)
.work_directory_abs_path
.as_ref(),
- Path::new(path!("/root/home"))
+ home,
);
});
}
@@ -23,7 +23,6 @@ doctest = false
alacritty_terminal.workspace = true
anyhow.workspace = true
collections.workspace = true
-dirs.workspace = true
futures.workspace = true
gpui.workspace = true
libc.workspace = true
@@ -48,7 +48,10 @@ use terminal_hyperlinks::RegexSearches;
use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
use theme::{ActiveTheme, Theme};
use urlencoding;
-use util::{paths::home_dir, truncate_and_trailoff};
+use util::{
+ paths::{self, home_dir},
+ truncate_and_trailoff,
+};
use std::{
borrow::Cow,
@@ -291,12 +294,11 @@ impl TerminalError {
Err(s) => s,
}
})
- .unwrap_or_else(|| match dirs::home_dir() {
- Some(dir) => format!(
+ .unwrap_or_else(|| {
+ format!(
"<none specified, using home directory> {}",
- dir.into_os_string().to_string_lossy()
- ),
- None => "<none specified, could not find home directory>".to_string(),
+ paths::home_dir().to_string_lossy()
+ )
})
}
@@ -546,9 +546,10 @@ mod tests {
let (workspace, cx) =
app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cwd = std::env::current_dir().expect("Failed to get working directory");
let terminal = project
.update(cx, |project: &mut Project, cx| {
- project.create_terminal_shell(None, cx)
+ project.create_terminal_shell(Some(cwd), cx)
})
.await
.expect("Failed to create a terminal");
@@ -12,10 +12,30 @@ use std::{
sync::LazyLock,
};
+static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
+
/// Returns the path to the user's home directory.
pub fn home_dir() -> &'static PathBuf {
- static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
- HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
+ HOME_DIR.get_or_init(|| {
+ if cfg!(any(test, feature = "test-support")) {
+ if cfg!(target_os = "macos") {
+ PathBuf::from("/Users/zed")
+ } else if cfg!(target_os = "windows") {
+ PathBuf::from("C:\\Users\\zed")
+ } else {
+ PathBuf::from("/home/zed")
+ }
+ } else {
+ dirs::home_dir().expect("failed to determine home directory")
+ }
+ })
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn set_home_dir(path: PathBuf) {
+ HOME_DIR
+ .set(path)
+ .expect("set_home_dir called after home_dir was already accessed");
}
pub trait PathExt {
@@ -55,6 +55,7 @@ collections = { workspace = true, features = ["test-support"] }
git2.workspace = true
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
+paths = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
rpc = { workspace = true, features = ["test-support"] }
@@ -1,34 +1,60 @@
use ignore::gitignore::Gitignore;
use std::{ffi::OsStr, path::Path, sync::Arc};
+#[derive(Clone, Debug)]
+pub struct IgnoreStack {
+ pub repo_root: Option<Arc<Path>>,
+ pub top: Arc<IgnoreStackEntry>,
+}
+
#[derive(Debug)]
-pub enum IgnoreStack {
+pub enum IgnoreStackEntry {
None,
+ Global {
+ ignore: Arc<Gitignore>,
+ },
Some {
abs_base_path: Arc<Path>,
ignore: Arc<Gitignore>,
- parent: Arc<IgnoreStack>,
+ parent: Arc<IgnoreStackEntry>,
},
All,
}
impl IgnoreStack {
- pub fn none() -> Arc<Self> {
- Arc::new(Self::None)
+ pub fn none() -> Self {
+ Self {
+ repo_root: None,
+ top: Arc::new(IgnoreStackEntry::None),
+ }
}
- pub fn all() -> Arc<Self> {
- Arc::new(Self::All)
+ pub fn all() -> Self {
+ Self {
+ repo_root: None,
+ top: Arc::new(IgnoreStackEntry::All),
+ }
+ }
+
+ pub fn global(ignore: Arc<Gitignore>) -> Self {
+ Self {
+ repo_root: None,
+ top: Arc::new(IgnoreStackEntry::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,
- _ => Arc::new(Self::Some {
+ pub fn append(self, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Self {
+ let top = match self.top.as_ref() {
+ IgnoreStackEntry::All => self.top.clone(),
+ _ => Arc::new(IgnoreStackEntry::Some {
abs_base_path,
ignore,
- parent: self,
+ parent: self.top.clone(),
}),
+ };
+ Self {
+ repo_root: self.repo_root,
+ top,
}
}
@@ -37,15 +63,37 @@ impl IgnoreStack {
return true;
}
- match self {
- Self::None => false,
- Self::All => true,
- Self::Some {
+ match self.top.as_ref() {
+ IgnoreStackEntry::None => false,
+ IgnoreStackEntry::All => true,
+ IgnoreStackEntry::Global { ignore } => {
+ let combined_path;
+ let abs_path = if let Some(repo_root) = self.repo_root.as_ref() {
+ combined_path = ignore.path().join(
+ abs_path
+ .strip_prefix(repo_root)
+ .expect("repo root should be a parent of matched path"),
+ );
+ &combined_path
+ } else {
+ abs_path
+ };
+ match ignore.matched(abs_path, is_dir) {
+ ignore::Match::None => false,
+ ignore::Match::Ignore(_) => true,
+ ignore::Match::Whitelist(_) => false,
+ }
+ }
+ IgnoreStackEntry::Some {
abs_base_path,
ignore,
parent: prev,
} => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
- ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
+ ignore::Match::None => IgnoreStack {
+ repo_root: self.repo_root.clone(),
+ top: prev.clone(),
+ }
+ .is_abs_path_ignored(abs_path, is_dir),
ignore::Match::Ignore(_) => true,
ignore::Match::Whitelist(_) => false,
},
@@ -65,7 +65,7 @@ use std::{
use sum_tree::{Bias, Dimensions, 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;
@@ -336,26 +336,10 @@ impl Default for WorkDirectory {
}
}
-#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
-pub struct WorkDirectoryEntry(ProjectEntryId);
-
-impl Deref for WorkDirectoryEntry {
- type Target = ProjectEntryId;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl From<ProjectEntryId> for WorkDirectoryEntry {
- fn from(value: ProjectEntryId) -> Self {
- WorkDirectoryEntry(value)
- }
-}
-
#[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)>,
@@ -504,6 +488,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(),
@@ -2807,8 +2792,9 @@ impl LocalSnapshot {
inodes
}
- fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
+ fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool, fs: &dyn Fs) -> IgnoreStack {
let mut new_ignores = Vec::new();
+ let mut repo_root = None;
for (index, ancestor) in abs_path.ancestors().enumerate() {
if index > 0 {
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
@@ -2817,12 +2803,21 @@ impl LocalSnapshot {
new_ignores.push((ancestor, None));
}
}
- if ancestor.join(*DOT_GIT).exists() {
+ let metadata = smol::block_on(fs.metadata(&ancestor.join(*DOT_GIT)))
+ .ok()
+ .flatten();
+ if metadata.is_some() {
+ repo_root = Some(Arc::from(ancestor));
break;
}
}
- let mut ignore_stack = IgnoreStack::none();
+ let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() {
+ IgnoreStack::global(global_gitignore)
+ } else {
+ IgnoreStack::none()
+ };
+ 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) {
ignore_stack = IgnoreStack::all();
@@ -2949,9 +2944,15 @@ impl BackgroundScannerState {
.any(|p| entry.path.starts_with(p))
}
- fn enqueue_scan_dir(&self, abs_path: Arc<Path>, entry: &Entry, scan_job_tx: &Sender<ScanJob>) {
+ fn enqueue_scan_dir(
+ &self,
+ abs_path: Arc<Path>,
+ entry: &Entry,
+ scan_job_tx: &Sender<ScanJob>,
+ fs: &dyn Fs,
+ ) {
let path = entry.path.clone();
- let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
+ let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true, fs);
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
if !ancestor_inodes.contains(&entry.inode) {
@@ -3860,14 +3861,36 @@ impl BackgroundScanner {
log::trace!("containing git repository: {containing_git_repository:?}");
+ let global_gitignore_path = paths::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();
state.snapshot.scan_id += 1;
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
- let ignore_stack = state
- .snapshot
- .ignore_stack_for_abs_path(root_abs_path.as_path(), true);
+ let ignore_stack = state.snapshot.ignore_stack_for_abs_path(
+ root_abs_path.as_path(),
+ true,
+ self.fs.as_ref(),
+ );
if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
@@ -3876,6 +3899,7 @@ impl BackgroundScanner {
SanitizedPath::cast_arc(root_abs_path),
&root_entry,
&scan_job_tx,
+ self.fs.as_ref(),
);
}
};
@@ -3946,6 +3970,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;
+ }
+ _ => {},
+ }
+ }
}
}
}
@@ -4130,11 +4163,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;
}
{
@@ -4147,6 +4189,32 @@ 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;
+ let abs_path = state.snapshot.abs_path().clone();
+ let ignore_stack =
+ state
+ .snapshot
+ .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref());
+ (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();
{
@@ -4158,7 +4226,12 @@ impl BackgroundScanner {
&& entry.kind == EntryKind::UnloadedDir
{
let abs_path = root_path.as_path().join(ancestor);
- state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
+ state.enqueue_scan_dir(
+ abs_path.into(),
+ entry,
+ &scan_job_tx,
+ self.fs.as_ref(),
+ );
state.paths_to_scan.insert(path.clone());
break;
}
@@ -4309,6 +4382,12 @@ impl BackgroundScanner {
swap_to_front(&mut child_paths, *GITIGNORE);
swap_to_front(&mut child_paths, *DOT_GIT);
+ if let Some(path) = child_paths.first()
+ && path.ends_with(*DOT_GIT)
+ {
+ ignore_stack.repo_root = Some(job.abs_path.clone());
+ }
+
for child_abs_path in child_paths {
let child_abs_path: Arc<Path> = child_abs_path.into();
let child_name = child_abs_path.file_name().unwrap();
@@ -4534,9 +4613,11 @@ impl BackgroundScanner {
let abs_path: Arc<Path> = root_abs_path.as_path().join(path).into();
match metadata {
Ok(Some((metadata, canonical_path))) => {
- let ignore_stack = state
- .snapshot
- .ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
+ let ignore_stack = state.snapshot.ignore_stack_for_abs_path(
+ &abs_path,
+ metadata.is_dir,
+ self.fs.as_ref(),
+ );
let is_external = !canonical_path.starts_with(&root_canonical_path);
let mut fs_entry = Entry::new(
path.clone(),
@@ -4561,7 +4642,12 @@ impl BackgroundScanner {
|| (fs_entry.path.as_os_str().is_empty()
&& abs_path.file_name() == Some(*DOT_GIT))
{
- state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx);
+ state.enqueue_scan_dir(
+ abs_path,
+ &fs_entry,
+ scan_queue_tx,
+ self.fs.as_ref(),
+ );
} else {
fs_entry.kind = EntryKind::UnloadedDir;
}
@@ -4616,43 +4702,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>, 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()
- .is_some_and(|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,
@@ -4662,8 +4720,6 @@ impl BackgroundScanner {
})
.unwrap();
}
-
- prev_snapshot = snapshot.clone();
}
drop(ignore_queue_tx);
@@ -4695,6 +4751,57 @@ impl BackgroundScanner {
.await;
}
+ fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
+ let mut ignores_to_update = Vec::new();
+
+ {
+ 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
+ }
+
+ 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();
+ }
+ 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) {
log::trace!("update ignore status {:?}", job.abs_path);
@@ -4710,6 +4817,12 @@ impl BackgroundScanner {
.strip_prefix(snapshot.abs_path.as_path())
.unwrap();
+ 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() {
let was_ignored = entry.is_ignored;
let abs_path: Arc<Path> = snapshot.abs_path().join(&entry.path).into();
@@ -4726,7 +4839,12 @@ impl BackgroundScanner {
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock();
if state.should_scan_directory(&entry) {
- state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue);
+ state.enqueue_scan_dir(
+ abs_path.clone(),
+ &entry,
+ &job.scan_queue,
+ self.fs.as_ref(),
+ );
}
}
@@ -4766,10 +4884,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
@@ -4791,8 +4910,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(),
@@ -4830,7 +4953,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) {
@@ -4870,7 +5001,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;
@@ -5052,7 +5183,7 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
struct ScanJob {
abs_path: Arc<Path>,
path: Arc<Path>,
- ignore_stack: Arc<IgnoreStack>,
+ ignore_stack: IgnoreStack,
scan_queue: Sender<ScanJob>,
ancestor_inodes: TreeSet<u64>,
is_external: bool,
@@ -5060,7 +5191,7 @@ struct ScanJob {
struct UpdateIgnoreStatusJob {
abs_path: Arc<Path>,
- ignore_stack: Arc<IgnoreStack>,
+ ignore_stack: IgnoreStack,
ignore_queue: Sender<UpdateIgnoreStatusJob>,
scan_queue: Sender<ScanJob>,
}
@@ -2120,7 +2120,6 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA
});
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
- eprintln!(">>>>>>>>>> touch");
fs.touch_path(path!("/root/subproject")).await;
worktree
.update(cx, |worktree, _| {
@@ -2141,6 +2140,117 @@ async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestA
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
}
+#[gpui::test]
+async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let home = paths::home_dir();
+ let fs = FakeFs::new(executor);
+ fs.insert_tree(
+ home,
+ json!({
+ ".config": {
+ "git": {
+ "ignore": "foo\n/bar\nbaz\n"
+ }
+ },
+ "project": {
+ ".git": {},
+ ".gitignore": "!baz",
+ "foo": "",
+ "bar": "",
+ "sub": {
+ "bar": "",
+ },
+ "subrepo": {
+ ".git": {},
+ "bar": ""
+ },
+ "baz": ""
+ }
+ }),
+ )
+ .await;
+ let worktree = Worktree::local(
+ home.join("project"),
+ true,
+ fs.clone(),
+ Arc::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ worktree
+ .update(cx, |worktree, _| {
+ worktree.as_local().unwrap().scan_complete()
+ })
+ .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,
+ &[],
+ &["foo", "bar", "subrepo/bar"],
+ &["sub/bar", "baz"],
+ &[],
+ );
+ });
+
+ // Ignore statuses are updated when excludesFile changes
+ fs.write(
+ &home.join(".config").join("git").join("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"],
+ &[],
+ );
+ });
+
+ // Statuses are updated when .git added/removed
+ fs.remove_dir(
+ &home.join("project").join("subrepo").join(".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]
fn check_worktree_entries(
tree: &Worktree,