Compare commits
17 commits
main
...
global-git
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e2b340a007 | ||
![]() |
09372534cd | ||
![]() |
a9a23251b7 | ||
![]() |
ca458cc529 | ||
![]() |
17822762b7 | ||
![]() |
60a64c3e24 | ||
![]() |
37336c6211 | ||
![]() |
890ff160d7 | ||
![]() |
1d990806df | ||
![]() |
ed685af557 | ||
![]() |
3119d0d06e | ||
![]() |
8b0203e177 | ||
![]() |
8792bf839b | ||
![]() |
f0da50bcb7 | ||
![]() |
896248dea3 | ||
![]() |
ac25d05e27 | ||
![]() |
dfae07e69c |
10 changed files with 424 additions and 121 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -11474,6 +11474,7 @@ name = "paths"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"ignore",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
|
|
@ -131,7 +131,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<()>;
|
||||
fn is_fake(&self) -> bool;
|
||||
|
@ -879,10 +878,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")))]
|
||||
|
@ -917,7 +912,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"))]
|
||||
|
@ -1104,7 +1098,6 @@ impl FakeFs {
|
|||
metadata_call_count: 0,
|
||||
path_write_counts: Default::default(),
|
||||
moves: Default::default(),
|
||||
home_dir: None,
|
||||
})),
|
||||
});
|
||||
|
||||
|
@ -1763,10 +1756,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"))]
|
||||
|
@ -2364,10 +2353,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
|
||||
|
|
|
@ -507,3 +507,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(Path::new(util::path!("/home/zed/.config/git/ignore")).into())
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ use lsp::{
|
|||
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};
|
||||
|
@ -1179,7 +1179,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,
|
||||
&[
|
||||
|
@ -7716,9 +7718,9 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
|
|||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
path!("/home"),
|
||||
json!({
|
||||
"home": {
|
||||
"zed": {
|
||||
".git": {},
|
||||
"project": {
|
||||
"a.txt": "A"
|
||||
|
@ -7727,9 +7729,8 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
|
|||
}),
|
||||
)
|
||||
.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(), [path!("/home/zed/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());
|
||||
|
||||
|
@ -7746,7 +7747,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(), [path!("/home/zed").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
|
||||
|
@ -7766,7 +7767,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"))
|
||||
Path::new(path!("/home/zed"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,7 +16,13 @@ use crate::NumericPrefixWithSuffix;
|
|||
/// 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")) {
|
||||
PathBuf::from("/home/zed")
|
||||
} else {
|
||||
dirs::home_dir().expect("failed to determine home directory")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 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 global(ignore: Arc<Gitignore>) -> Self {
|
||||
Self {
|
||||
repo_root: None,
|
||||
top: Arc::new(IgnoreStackEntry::Global { ignore }),
|
||||
}
|
||||
}
|
||||
|
||||
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, 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(),
|
||||
|
@ -2785,8 +2770,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) {
|
||||
|
@ -2795,12 +2781,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();
|
||||
|
@ -2927,9 +2922,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) {
|
||||
|
@ -3845,19 +3846,46 @@ 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());
|
||||
}
|
||||
state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx);
|
||||
state.enqueue_scan_dir(
|
||||
root_abs_path.into(),
|
||||
&root_entry,
|
||||
&scan_job_tx,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3927,6 +3955,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;
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4109,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;
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -4126,6 +4172,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();
|
||||
{
|
||||
|
@ -4136,7 +4208,12 @@ impl BackgroundScanner {
|
|||
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
|
||||
if 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;
|
||||
}
|
||||
|
@ -4289,6 +4366,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();
|
||||
|
@ -4514,9 +4597,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(),
|
||||
|
@ -4541,7 +4626,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;
|
||||
}
|
||||
|
@ -4597,43 +4687,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()
|
||||
.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,
|
||||
|
@ -4643,8 +4705,6 @@ impl BackgroundScanner {
|
|||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
prev_snapshot = snapshot.clone();
|
||||
}
|
||||
drop(ignore_queue_tx);
|
||||
|
||||
|
@ -4676,6 +4736,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);
|
||||
|
||||
|
@ -4691,6 +4802,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();
|
||||
|
@ -4707,7 +4824,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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4747,10 +4869,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
|
||||
|
@ -4770,8 +4893,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(),
|
||||
|
@ -4811,7 +4938,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) {
|
||||
|
@ -4851,7 +4986,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;
|
||||
|
@ -5033,7 +5168,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,
|
||||
|
@ -5041,7 +5176,7 @@ struct ScanJob {
|
|||
|
||||
struct UpdateIgnoreStatusJob {
|
||||
abs_path: Arc<Path>,
|
||||
ignore_stack: Arc<IgnoreStack>,
|
||||
ignore_stack: IgnoreStack,
|
||||
ignore_queue: Sender<UpdateIgnoreStatusJob>,
|
||||
scan_queue: Sender<ScanJob>,
|
||||
}
|
||||
|
|
|
@ -2025,7 +2025,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, _| {
|
||||
|
@ -2046,6 +2045,116 @@ 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 fs = FakeFs::new(executor);
|
||||
fs.insert_tree(
|
||||
path!("/home/zed"),
|
||||
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(
|
||||
path!("/home/zed/project").as_ref(),
|
||||
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(
|
||||
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"],
|
||||
&[],
|
||||
);
|
||||
});
|
||||
|
||||
// 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]
|
||||
fn check_worktree_entries(
|
||||
tree: &Worktree,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue