Merge remote-tracking branch 'origin/main' into global-gitignore

This commit is contained in:
Cole Miller 2025-06-06 11:26:00 -04:00
commit f0da50bcb7
1511 changed files with 192222 additions and 71443 deletions

View file

@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{Context as _, Result, anyhow};
use clock::ReplicaId;
use collections::{HashMap, HashSet, VecDeque};
use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive};
use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};
use futures::{
FutureExt as _, Stream, StreamExt,
channel::{
@ -29,7 +29,7 @@ use ignore::IgnoreStack;
use language::DiskState;
use parking_lot::Mutex;
use paths::local_settings_folder_relative_path;
use paths::{local_settings_folder_relative_path, local_vscode_folder_relative_path};
use postage::{
barrier,
prelude::{Sink as _, Stream as _},
@ -45,6 +45,7 @@ use smallvec::{SmallVec, smallvec};
use smol::channel::{self, Sender};
use std::{
any::Any,
borrow::Borrow as _,
cmp::Ordering,
collections::hash_map,
convert::TryFrom,
@ -106,6 +107,15 @@ pub struct LoadedBinaryFile {
pub content: Vec<u8>,
}
impl fmt::Debug for LoadedBinaryFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LoadedBinaryFile")
.field("file", &self.file)
.field("content_bytes", &self.content.len())
.finish()
}
}
pub struct LocalWorktree {
snapshot: LocalSnapshot,
scan_requests_tx: channel::Sender<ScanRequest>,
@ -242,13 +252,7 @@ impl WorkDirectory {
match self {
WorkDirectory::InProject { relative_path } => Ok(path
.strip_prefix(relative_path)
.map_err(|_| {
anyhow!(
"could not relativize {:?} against {:?}",
path,
relative_path
)
})?
.map_err(|_| anyhow!("could not relativize {path:?} against {relative_path:?}"))?
.into()),
WorkDirectory::AboveProject {
location_in_repo, ..
@ -383,12 +387,23 @@ struct LocalRepositoryEntry {
work_directory: WorkDirectory,
work_directory_abs_path: Arc<Path>,
git_dir_scan_id: usize,
original_dot_git_abs_path: Arc<Path>,
/// Absolute path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
dot_git_dir_abs_path: Arc<Path>,
/// Absolute path to the .git file, if we're in a git worktree.
dot_git_worktree_abs_path: Option<Arc<Path>>,
/// Absolute path to the original .git entry that caused us to create this repository.
///
/// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere
/// (whose path we then store in `repository_dir_abs_path`).
dot_git_abs_path: Arc<Path>,
/// Absolute path to the "commondir" for this repository.
///
/// This is always a directory. For a normal repository, this is the same as dot_git_abs_path,
/// but in the case of a submodule or a worktree it is the path to the "parent" .git directory
/// from which the submodule/worktree was derived.
common_dir_abs_path: Arc<Path>,
/// Absolute path to the directory holding the repository's state.
///
/// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and
/// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the
/// commondir like `/project/.git/modules/foo`.
repository_dir_abs_path: Arc<Path>,
}
impl sum_tree::Item for LocalRepositoryEntry {
@ -835,18 +850,20 @@ impl Worktree {
&mut self,
path: impl Into<Arc<Path>>,
is_directory: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
let path: Arc<Path> = path.into();
let worktree_id = self.id();
match self {
Worktree::Local(this) => this.create_entry(path, is_directory, cx),
Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
Worktree::Remote(this) => {
let project_id = this.project_id;
let request = this.client.request(proto::CreateProjectEntry {
worktree_id: worktree_id.to_proto(),
project_id,
path: path.as_ref().to_proto(),
content,
is_directory,
});
cx.spawn(async move |this, cx| {
@ -863,7 +880,7 @@ impl Worktree {
.await
.map(CreatedEntry::Included),
None => {
let abs_path = this.update(cx, |worktree, _| {
let abs_path = this.read_with(cx, |worktree, _| {
worktree
.absolutize(&path)
.with_context(|| format!("absolutizing {path:?}"))
@ -967,18 +984,14 @@ impl Worktree {
pub fn copy_external_entries(
&mut self,
target_directory: PathBuf,
target_directory: Arc<Path>,
paths: Vec<Arc<Path>>,
overwrite_existing_files: bool,
fs: Arc<dyn Fs>,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>>> {
match self {
Worktree::Local(this) => {
this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx)
}
_ => Task::ready(Err(anyhow!(
"Copying external entries is not supported for remote worktrees"
))),
Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx),
Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx),
}
}
@ -1045,6 +1058,7 @@ impl Worktree {
this.create_entry(
Arc::<Path>::from_proto(request.path),
request.is_directory,
request.content,
cx,
),
)
@ -1073,7 +1087,7 @@ impl Worktree {
),
)
})?;
task.ok_or_else(|| anyhow!("invalid entry"))?.await?;
task.context("invalid entry")?.await?;
Ok(proto::ProjectEntryResponse {
entry: None,
worktree_scan_id: scan_id as u64,
@ -1088,7 +1102,7 @@ impl Worktree {
let task = this.update(&mut cx, |this, cx| {
this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx)
})?;
task.ok_or_else(|| anyhow!("no such entry"))?.await?;
task.context("no such entry")?.await?;
let scan_id = this.read_with(&cx, |this, _| this.scan_id())?;
Ok(proto::ExpandProjectEntryResponse {
worktree_scan_id: scan_id as u64,
@ -1103,7 +1117,7 @@ impl Worktree {
let task = this.update(&mut cx, |this, cx| {
this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx)
})?;
task.ok_or_else(|| anyhow!("no such entry"))?.await?;
task.context("no such entry")?.await?;
let scan_id = this.read_with(&cx, |this, _| this.scan_id())?;
Ok(proto::ExpandAllForProjectEntryResponse {
worktree_scan_id: scan_id as u64,
@ -1171,6 +1185,31 @@ impl Worktree {
pub fn is_single_file(&self) -> bool {
self.root_dir().is_none()
}
/// For visible worktrees, returns the path with the worktree name as the first component.
/// Otherwise, returns an absolute path.
pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
let mut full_path = PathBuf::new();
if self.is_visible() {
full_path.push(self.root_name());
} else {
let path = self.abs_path();
if self.is_local() && path.starts_with(home_dir().as_path()) {
full_path.push("~");
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
} else {
full_path.push(path)
}
}
if worktree_relative_path.components().next().is_some() {
full_path.push(&worktree_relative_path);
}
full_path
}
}
impl LocalWorktree {
@ -1325,7 +1364,11 @@ impl LocalWorktree {
new_work_directory_abs_path: Some(
new_repo.work_directory_abs_path.clone(),
),
dot_git_abs_path: Some(new_repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(
new_repo.repository_dir_abs_path.clone(),
),
common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
});
new_repos.next();
}
@ -1342,9 +1385,11 @@ impl LocalWorktree {
new_work_directory_abs_path: Some(
new_repo.work_directory_abs_path.clone(),
),
dot_git_abs_path: Some(
new_repo.original_dot_git_abs_path.clone(),
dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(
new_repo.repository_dir_abs_path.clone(),
),
common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
});
}
new_repos.next();
@ -1358,6 +1403,8 @@ impl LocalWorktree {
),
new_work_directory_abs_path: None,
dot_git_abs_path: None,
repository_dir_abs_path: None,
common_dir_abs_path: None,
});
old_repos.next();
}
@ -1368,7 +1415,9 @@ impl LocalWorktree {
work_directory_id: entry_id,
old_work_directory_abs_path: None,
new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
});
new_repos.next();
}
@ -1377,7 +1426,9 @@ impl LocalWorktree {
work_directory_id: entry_id,
old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
new_work_directory_abs_path: None,
dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
});
old_repos.next();
}
@ -1430,9 +1481,7 @@ impl LocalWorktree {
let abs_path = abs_path?;
let content = fs.load_bytes(&abs_path).await?;
let worktree = worktree
.upgrade()
.ok_or_else(|| anyhow!("worktree was dropped"))?;
let worktree = worktree.upgrade().context("worktree was dropped")?;
let file = match entry.await? {
Some(entry) => File::for_entry(entry, worktree),
None => {
@ -1487,9 +1536,7 @@ impl LocalWorktree {
}
let text = fs.load(&abs_path).await?;
let worktree = this
.upgrade()
.ok_or_else(|| anyhow!("worktree was dropped"))?;
let worktree = this.upgrade().context("worktree was dropped")?;
let file = match entry.await? {
Some(entry) => File::for_entry(entry, worktree),
None => {
@ -1536,6 +1583,7 @@ impl LocalWorktree {
&self,
path: impl Into<Arc<Path>>,
is_dir: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
let path = path.into();
@ -1552,7 +1600,7 @@ impl LocalWorktree {
.await
.with_context(|| format!("creating directory {task_abs_path:?}"))
} else {
fs.save(&task_abs_path, &Rope::default(), LineEnding::default())
fs.write(&task_abs_path, content.as_deref().unwrap_or(&[]))
.await
.with_context(|| format!("creating file {task_abs_path:?}"))
}
@ -1625,7 +1673,7 @@ impl LocalWorktree {
.refresh_entry(path.clone(), None, cx)
})?
.await?;
let worktree = this.upgrade().ok_or_else(|| anyhow!("worktree dropped"))?;
let worktree = this.upgrade().context("worktree dropped")?;
if let Some(entry) = entry {
Ok(File::for_entry(entry, worktree))
} else {
@ -1828,11 +1876,13 @@ impl LocalWorktree {
pub fn copy_external_entries(
&self,
target_directory: PathBuf,
target_directory: Arc<Path>,
paths: Vec<Arc<Path>>,
overwrite_existing_files: bool,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>>> {
let Ok(target_directory) = self.absolutize(&target_directory) else {
return Task::ready(Err(anyhow!("invalid target path")));
};
let worktree_path = self.abs_path().clone();
let fs = self.fs.clone();
let paths = paths
@ -1864,23 +1914,23 @@ impl LocalWorktree {
&source,
&target,
fs::CopyOptions {
overwrite: overwrite_existing_files,
overwrite: true,
..Default::default()
},
)
.await
.with_context(|| {
anyhow!("Failed to copy file from {source:?} to {target:?}")
format!("Failed to copy file from {source:?} to {target:?}")
})?;
}
Ok::<(), anyhow::Error>(())
anyhow::Ok(())
})
.await
.log_err();
let mut refresh = cx.read_entity(
&this.upgrade().with_context(|| "Dropped worktree")?,
|this, _| {
Ok::<postage::barrier::Receiver, anyhow::Error>(
anyhow::Ok::<postage::barrier::Receiver>(
this.as_local()
.with_context(|| "Worktree is not local")?
.refresh_entries_for_paths(paths_to_refresh.clone()),
@ -1890,7 +1940,7 @@ impl LocalWorktree {
cx.background_spawn(async move {
refresh.next().await;
Ok::<(), anyhow::Error>(())
anyhow::Ok(())
})
.await
.log_err();
@ -1977,10 +2027,10 @@ impl LocalWorktree {
cx.spawn(async move |this, cx| {
refresh.recv().await;
log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed());
let new_entry = this.update(cx, |this, _| {
let new_entry = this.read_with(cx, |this, _| {
this.entry_for_path(path)
.cloned()
.ok_or_else(|| anyhow!("failed to read path after update"))
.context("reading path after update")
})??;
Ok(Some(new_entry))
})
@ -2224,7 +2274,7 @@ impl RemoteWorktree {
.await
.map(CreatedEntry::Included),
None => {
let abs_path = this.update(cx, |worktree, _| {
let abs_path = this.read_with(cx, |worktree, _| {
worktree
.absolutize(&new_path)
.with_context(|| format!("absolutizing {new_path:?}"))
@ -2234,6 +2284,62 @@ impl RemoteWorktree {
}
})
}
fn copy_external_entries(
&self,
target_directory: Arc<Path>,
paths_to_copy: Vec<Arc<Path>>,
local_fs: Arc<dyn Fs>,
cx: &Context<Worktree>,
) -> Task<anyhow::Result<Vec<ProjectEntryId>>> {
let client = self.client.clone();
let worktree_id = self.id().to_proto();
let project_id = self.project_id;
cx.background_spawn(async move {
let mut requests = Vec::new();
for root_path_to_copy in paths_to_copy {
let Some(filename) = root_path_to_copy.file_name() else {
continue;
};
for (abs_path, is_directory) in
read_dir_items(local_fs.as_ref(), &root_path_to_copy).await?
{
let Ok(relative_path) = abs_path.strip_prefix(&root_path_to_copy) else {
continue;
};
let content = if is_directory {
None
} else {
Some(local_fs.load_bytes(&abs_path).await?)
};
let mut target_path = target_directory.join(filename);
if relative_path.file_name().is_some() {
target_path.push(relative_path)
}
requests.push(proto::CreateProjectEntry {
project_id,
worktree_id,
path: target_path.to_string_lossy().to_string(),
is_directory,
content,
});
}
}
requests.sort_unstable_by(|a, b| a.path.cmp(&b.path));
requests.dedup();
let mut copied_entry_ids = Vec::new();
for request in requests {
let response = client.request(request).await?;
copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id)));
}
Ok(copied_entry_ids)
})
}
}
impl Snapshot {
@ -2308,7 +2414,7 @@ impl Snapshot {
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(anyhow!("invalid path"));
anyhow::bail!("invalid path");
}
if path.file_name().is_some() {
Ok(self.abs_path.as_path().join(path))
@ -2541,7 +2647,7 @@ impl Snapshot {
}
pub fn root_entry(&self) -> Option<&Entry> {
self.entry_for_path("")
self.entries_by_path.first()
}
/// TODO: what's the difference between `root_dir` and `abs_path`?
@ -2816,6 +2922,7 @@ impl BackgroundScannerState {
(!entry.is_external && (!entry.is_ignored || entry.is_always_included))
|| entry.path.file_name() == Some(*DOT_GIT)
|| entry.path.file_name() == Some(local_settings_folder_relative_path().as_os_str())
|| entry.path.file_name() == Some(local_vscode_folder_relative_path().as_os_str())
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|| self
.paths_to_scan
@ -3016,9 +3123,6 @@ impl BackgroundScannerState {
);
return;
};
log::debug!(
"building git repository, `.git` path in the worktree: {dot_git_path:?}"
);
parent_dir.into()
}
@ -3049,7 +3153,6 @@ impl BackgroundScannerState {
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<LocalRepositoryEntry> {
log::trace!("insert git repository for {dot_git_path:?}");
let work_dir_entry = self.snapshot.entry_for_path(work_directory.path_key().0)?;
let work_directory_abs_path = self
.snapshot
@ -3066,46 +3169,31 @@ impl BackgroundScannerState {
return None;
}
let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
let dot_git_abs_path: Arc<Path> = self
.snapshot
.abs_path
.as_path()
.join(&dot_git_path)
.as_path()
.into();
// TODO add these watchers without building a whole repository by parsing .git-with-indirection
let t0 = Instant::now();
let repository = fs.open_repo(&dot_git_abs_path)?;
log::trace!("opened git repo for {dot_git_abs_path:?}");
let repository_path = repository.path();
watcher.add(&repository_path).log_err()?;
let actual_dot_git_dir_abs_path = repository.main_repository_path();
let dot_git_worktree_abs_path = if actual_dot_git_dir_abs_path == dot_git_abs_path {
None
} else {
// The two paths could be different because we opened a git worktree.
// When that happens:
//
// * `dot_git_abs_path` is a file that points to the worktree-subdirectory in the actual
// .git directory.
//
// * `repository_path` is the worktree-subdirectory.
//
// * `actual_dot_git_dir_abs_path` is the path to the actual .git directory. In git
// documentation this is called the "commondir".
watcher.add(&dot_git_abs_path).log_err()?;
Some(Arc::from(dot_git_abs_path.as_path()))
};
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
let (repository_dir_abs_path, common_dir_abs_path) =
discover_git_paths(&dot_git_abs_path, fs);
watcher.add(&common_dir_abs_path).log_err();
if !repository_dir_abs_path.starts_with(&common_dir_abs_path) {
watcher.add(&repository_dir_abs_path).log_err();
}
let work_directory_id = work_dir_entry.id;
let local_repository = LocalRepositoryEntry {
work_directory_id,
work_directory,
git_dir_scan_id: 0,
original_dot_git_abs_path: dot_git_abs_path.as_path().into(),
dot_git_dir_abs_path: actual_dot_git_dir_abs_path.into(),
work_directory_abs_path: work_directory_abs_path.as_path().into(),
dot_git_worktree_abs_path,
git_dir_scan_id: 0,
dot_git_abs_path,
common_dir_abs_path,
repository_dir_abs_path,
};
self.snapshot
@ -3267,7 +3355,7 @@ impl fmt::Debug for Snapshot {
}
}
#[derive(Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct File {
pub worktree: Entity<Worktree>,
pub path: Arc<Path>,
@ -3291,27 +3379,7 @@ impl language::File for File {
}
fn full_path(&self, cx: &App) -> PathBuf {
let mut full_path = PathBuf::new();
let worktree = self.worktree.read(cx);
if worktree.is_visible() {
full_path.push(worktree.root_name());
} else {
let path = worktree.abs_path();
if worktree.is_local() && path.starts_with(home_dir().as_path()) {
full_path.push("~");
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
} else {
full_path.push(path)
}
}
if self.path.components().next().is_some() {
full_path.push(&self.path);
}
full_path
self.worktree.read(cx).full_path(&self.path)
}
/// Returns the last component of this handle's absolute path. If this handle refers to the root
@ -3326,10 +3394,6 @@ impl language::File for File {
self.worktree.read(cx).id()
}
fn as_any(&self) -> &dyn Any {
self
}
fn to_proto(&self, cx: &App) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.read(cx).id().to_proto(),
@ -3391,15 +3455,12 @@ impl File {
worktree: Entity<Worktree>,
cx: &App,
) -> Result<Self> {
let worktree_id = worktree
.read(cx)
.as_remote()
.ok_or_else(|| anyhow!("not remote"))?
.id();
let worktree_id = worktree.read(cx).as_remote().context("not remote")?.id();
if worktree_id.to_proto() != proto.worktree_id {
return Err(anyhow!("worktree id does not match file"));
}
anyhow::ensure!(
worktree_id.to_proto() == proto.worktree_id,
"worktree id does not match file"
);
let disk_state = if proto.is_deleted {
DiskState::Deleted
@ -3422,7 +3483,11 @@ impl File {
}
pub fn from_dyn(file: Option<&Arc<dyn language::File>>) -> Option<&Self> {
file.and_then(|f| f.as_any().downcast_ref())
file.and_then(|f| {
let f: &dyn language::File = f.borrow();
let f: &dyn Any = f;
f.downcast_ref()
})
}
pub fn worktree_id(&self, cx: &App) -> WorktreeId {
@ -3511,6 +3576,8 @@ pub struct UpdatedGitRepository {
/// For a normal git repository checkout, the absolute path to the .git directory.
/// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
pub dot_git_abs_path: Option<Arc<Path>>,
pub repository_dir_abs_path: Option<Arc<Path>>,
pub common_dir_abs_path: Option<Arc<Path>>,
}
pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
@ -3830,71 +3897,23 @@ impl BackgroundScanner {
// the git repository in an ancestor directory. Find any gitignore files
// in ancestor directories.
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
let mut containing_git_repository = None;
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
if index != 0 {
if Some(ancestor) == self.fs.home_dir().as_deref() {
// Unless $HOME is itself the worktree root, don't consider it as a
// containing git repository---expensive and likely unwanted.
break;
} else if let Ok(ignore) =
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
{
self.state
.lock()
.snapshot
.ignores_by_parent_abs_path
.insert(ancestor.into(), (ignore.into(), false));
}
}
let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
self.state
.lock()
.snapshot
.ignores_by_parent_abs_path
.extend(ignores);
let containing_git_repository = repo.and_then(|(ancestor_dot_git, work_directory)| {
self.state.lock().insert_git_repository_for_path(
work_directory,
ancestor_dot_git.as_path().into(),
self.fs.as_ref(),
self.watcher.as_ref(),
)?;
Some(ancestor_dot_git)
});
let ancestor_dot_git = ancestor.join(*DOT_GIT);
log::trace!("considering ancestor: {ancestor_dot_git:?}");
// 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 location_in_repo = root_abs_path
.as_path()
.strip_prefix(ancestor)
.unwrap()
.into();
log::info!(
"inserting parent git repo for this worktree: {location_in_repo:?}"
);
// We associate the external git repo with our root folder and
// also mark where in the git repo the root folder is located.
let local_repository = self.state.lock().insert_git_repository_for_path(
WorkDirectory::AboveProject {
absolute_path: ancestor.into(),
location_in_repo,
},
ancestor_dot_git.clone().into(),
self.fs.as_ref(),
self.watcher.as_ref(),
);
if local_repository.is_some() {
containing_git_repository = Some(ancestor_dot_git)
}
};
}
// Reached root of git repository.
break;
}
}
log::info!("containing git repository: {containing_git_repository:?}");
log::trace!("containing git repository: {containing_git_repository:?}");
let (scan_job_tx, scan_job_rx) = channel::unbounded();
{
@ -3992,7 +4011,7 @@ impl BackgroundScanner {
let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await {
Ok(path) => SanitizedPath::from(path),
Err(err) => {
log::error!("failed to canonicalize root path: {}", err);
log::error!("failed to canonicalize root path {root_path:?}: {err}");
return true;
}
};
@ -4115,8 +4134,8 @@ impl BackgroundScanner {
if abs_path.0.file_name() == Some(*GITIGNORE) {
for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) {
dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf());
if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.common_dir_abs_path.as_ref()) {
dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf());
}
}
}
@ -4175,7 +4194,6 @@ impl BackgroundScanner {
}
}
self.send_status_update(false, SmallVec::new());
// send_status_update_inner(phase, state, status_update_tx, false, SmallVec::new());
}
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
@ -4399,11 +4417,7 @@ impl BackgroundScanner {
let canonical_path = match self.fs.canonicalize(&child_abs_path).await {
Ok(path) => path,
Err(err) => {
log::error!(
"error reading target of symlink {:?}: {:?}",
child_abs_path,
err
);
log::error!("error reading target of symlink {child_abs_path:?}: {err:#}",);
continue;
}
};
@ -4544,6 +4558,15 @@ impl BackgroundScanner {
)
.await;
let mut new_ancestor_repo = if relative_paths
.iter()
.any(|path| path.as_ref() == Path::new(""))
{
Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await)
} else {
None
};
let mut state = self.state.lock();
let doing_recursive_update = scan_queue_tx.is_some();
@ -4595,6 +4618,21 @@ impl BackgroundScanner {
}
state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
if path.as_ref() == Path::new("") {
if let Some((ignores, repo)) = new_ancestor_repo.take() {
log::trace!("updating ancestor git repository");
state.snapshot.ignores_by_parent_abs_path.extend(ignores);
if let Some((ancestor_dot_git, work_directory)) = repo {
state.insert_git_repository_for_path(
work_directory,
ancestor_dot_git.as_path().into(),
self.fs.as_ref(),
self.watcher.as_ref(),
);
}
}
}
}
Ok(None) => {
self.remove_repo_path(path, &mut state.snapshot);
@ -4790,8 +4828,8 @@ impl BackgroundScanner {
.git_repositories
.iter()
.find_map(|(_, repo)| {
if repo.dot_git_dir_abs_path.as_ref() == &dot_git_dir
|| repo.dot_git_worktree_abs_path.as_deref() == Some(&dot_git_dir)
if repo.common_dir_abs_path.as_ref() == &dot_git_dir
|| repo.repository_dir_abs_path.as_ref() == &dot_git_dir
{
Some(repo.clone())
} else {
@ -4833,7 +4871,7 @@ impl BackgroundScanner {
if exists_in_snapshot
|| matches!(
smol::block_on(self.fs.metadata(&entry.dot_git_dir_abs_path)),
smol::block_on(self.fs.metadata(&entry.common_dir_abs_path)),
Ok(Some(_))
)
{
@ -4873,6 +4911,68 @@ impl BackgroundScanner {
}
}
async fn discover_ancestor_git_repo(
fs: Arc<dyn Fs>,
root_abs_path: &SanitizedPath,
) -> (
HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
Option<(PathBuf, WorkDirectory)>,
) {
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() {
// Unless $HOME is itself the worktree root, don't consider it as a
// containing git repository---expensive and likely unwanted.
break;
} else if let Ok(ignore) =
build_gitignore(&ancestor.join(*GITIGNORE), fs.as_ref()).await
{
ignores.insert(ancestor.into(), (ignore.into(), false));
}
}
let ancestor_dot_git = ancestor.join(*DOT_GIT);
log::trace!("considering ancestor: {ancestor_dot_git:?}");
// Check whether the directory or file called `.git` exists (in the
// case of worktrees it's a file.)
if 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) = fs.canonicalize(&ancestor_dot_git).await.log_err() {
let location_in_repo = root_abs_path
.as_path()
.strip_prefix(ancestor)
.unwrap()
.into();
log::info!("inserting parent git repo for this worktree: {location_in_repo:?}");
// We associate the external git repo with our root folder and
// also mark where in the git repo the root folder is located.
return (
ignores,
Some((
ancestor_dot_git,
WorkDirectory::AboveProject {
absolute_path: ancestor.into(),
location_in_repo,
},
)),
);
};
}
// Reached root of git repository.
break;
}
}
(ignores, None)
}
fn build_diff(
phase: BackgroundScannerPhase,
old_snapshot: &Snapshot,
@ -5045,7 +5145,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
let file_name = "fs-event-sentinel";
let tree = self.clone();
let (fs, root_path) = self.update(cx, |tree, _| {
let (fs, root_path) = self.read_with(cx, |tree, _| {
let tree = tree.as_local().unwrap();
(tree.fs.clone(), tree.abs_path().clone())
});
@ -5057,7 +5157,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
let mut events = cx.events(&tree);
while events.next().await.is_some() {
if tree.update(cx, |tree, _| tree.entry_for_path(file_name).is_some()) {
if tree.read_with(cx, |tree, _| tree.entry_for_path(file_name).is_some()) {
break;
}
}
@ -5066,7 +5166,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
.await
.unwrap();
while events.next().await.is_some() {
if tree.update(cx, |tree, _| tree.entry_for_path(file_name).is_none()) {
if tree.read_with(cx, |tree, _| tree.entry_for_path(file_name).is_none()) {
break;
}
}
@ -5091,7 +5191,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
let file_name = "fs-event-sentinel";
let tree = self.clone();
let (fs, root_path, mut git_dir_scan_id) = self.update(cx, |tree, _| {
let (fs, root_path, mut git_dir_scan_id) = self.read_with(cx, |tree, _| {
let tree = tree.as_local().unwrap();
let local_repo_entry = tree
.git_repositories
@ -5100,7 +5200,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
.unwrap();
(
tree.fs.clone(),
local_repo_entry.dot_git_dir_abs_path.clone(),
local_repo_entry.common_dir_abs_path.clone(),
local_repo_entry.git_dir_scan_id,
)
});
@ -5487,6 +5587,10 @@ impl ProjectEntryId {
self.0 as u64
}
pub fn from_usize(id: usize) -> Self {
ProjectEntryId(id)
}
pub fn to_usize(&self) -> usize {
self.0
}
@ -5501,3 +5605,40 @@ impl CreatedEntry {
}
}
}
fn parse_gitfile(content: &str) -> anyhow::Result<&Path> {
let path = content
.strip_prefix("gitdir:")
.with_context(|| format!("parsing gitfile content {content:?}"))?;
Ok(Path::new(path.trim()))
}
fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, Arc<Path>) {
let mut repository_dir_abs_path = dot_git_abs_path.clone();
let mut common_dir_abs_path = dot_git_abs_path.clone();
if let Some(path) = smol::block_on(fs.load(&dot_git_abs_path))
.ok()
.as_ref()
.and_then(|contents| parse_gitfile(contents).log_err())
{
let path = dot_git_abs_path
.parent()
.unwrap_or(Path::new(""))
.join(path);
if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() {
repository_dir_abs_path = Path::new(&path).into();
common_dir_abs_path = repository_dir_abs_path.clone();
if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok()
{
if let Some(commondir_path) =
smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err()
{
common_dir_abs_path = commondir_path.as_path().into();
}
}
}
};
(repository_dir_abs_path, common_dir_abs_path)
}