Fix ancestor git repositories going missing (#28436)
Closes #ISSUE Release Notes: - Fixed a bug that caused Zed to sometimes not discover git repositories above a worktree root.
This commit is contained in:
parent
c7963c8a93
commit
7bf6cd4ccf
2 changed files with 164 additions and 64 deletions
|
@ -3768,69 +3768,21 @@ impl BackgroundScanner {
|
||||||
// the git repository in an ancestor directory. Find any gitignore files
|
// the git repository in an ancestor directory. Find any gitignore files
|
||||||
// in ancestor directories.
|
// in ancestor directories.
|
||||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
let mut containing_git_repository = None;
|
let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
|
||||||
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
|
self.state
|
||||||
if index != 0 {
|
.lock()
|
||||||
if Some(ancestor) == self.fs.home_dir().as_deref() {
|
.snapshot
|
||||||
// Unless $HOME is itself the worktree root, don't consider it as a
|
.ignores_by_parent_abs_path
|
||||||
// containing git repository---expensive and likely unwanted.
|
.extend(ignores);
|
||||||
break;
|
let containing_git_repository = repo.and_then(|(ancestor_dot_git, work_directory)| {
|
||||||
} else if let Ok(ignore) =
|
self.state.lock().insert_git_repository_for_path(
|
||||||
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
|
work_directory,
|
||||||
{
|
ancestor_dot_git.as_path().into(),
|
||||||
self.state
|
self.fs.as_ref(),
|
||||||
.lock()
|
self.watcher.as_ref(),
|
||||||
.snapshot
|
)?;
|
||||||
.ignores_by_parent_abs_path
|
Some(ancestor_dot_git)
|
||||||
.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 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::info!("containing git repository: {containing_git_repository:?}");
|
||||||
|
|
||||||
|
@ -4482,6 +4434,15 @@ impl BackgroundScanner {
|
||||||
)
|
)
|
||||||
.await;
|
.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 mut state = self.state.lock();
|
||||||
let doing_recursive_update = scan_queue_tx.is_some();
|
let doing_recursive_update = scan_queue_tx.is_some();
|
||||||
|
|
||||||
|
@ -4533,6 +4494,21 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
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) => {
|
Ok(None) => {
|
||||||
self.remove_repo_path(path, &mut state.snapshot);
|
self.remove_repo_path(path, &mut state.snapshot);
|
||||||
|
@ -4811,6 +4787,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(
|
fn build_diff(
|
||||||
phase: BackgroundScannerPhase,
|
phase: BackgroundScannerPhase,
|
||||||
old_snapshot: &Snapshot,
|
old_snapshot: &Snapshot,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||||
use git::GITIGNORE;
|
use git::GITIGNORE;
|
||||||
use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
|
use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
@ -1984,6 +1984,68 @@ fn test_unrelativize() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(executor);
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/root"),
|
||||||
|
json!({
|
||||||
|
".git": {},
|
||||||
|
"subproject": {
|
||||||
|
"a.txt": "A"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let worktree = Worktree::local(
|
||||||
|
path!("/root/subproject").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();
|
||||||
|
let repos = worktree.update(cx, |worktree, _| {
|
||||||
|
worktree
|
||||||
|
.as_local()
|
||||||
|
.unwrap()
|
||||||
|
.git_repositories
|
||||||
|
.values()
|
||||||
|
.map(|entry| entry.work_directory_abs_path.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
|
||||||
|
|
||||||
|
eprintln!(">>>>>>>>>> touch");
|
||||||
|
fs.touch_path(path!("/root/subproject")).await;
|
||||||
|
worktree
|
||||||
|
.update(cx, |worktree, _| {
|
||||||
|
worktree.as_local().unwrap().scan_complete()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let repos = worktree.update(cx, |worktree, _| {
|
||||||
|
worktree
|
||||||
|
.as_local()
|
||||||
|
.unwrap()
|
||||||
|
.git_repositories
|
||||||
|
.values()
|
||||||
|
.map(|entry| entry.work_directory_abs_path.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn check_worktree_entries(
|
fn check_worktree_entries(
|
||||||
tree: &Worktree,
|
tree: &Worktree,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue