Merge remote-tracking branch 'origin/main' into global-gitignore
This commit is contained in:
commit
f0da50bcb7
1511 changed files with 192222 additions and 71443 deletions
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue