Worktree paths in git panel, take 2 (#26047)

Modified version of #25950. We still use worktree paths, but repo paths
with a status that lie outside the worktree are not excluded; instead,
we relativize them by adding `..`. This makes the list in the git panel
match what you'd get from running `git status` (with the repo's worktree
root as the working directory).

- [x] Implement + test new unrelativization logic
- [x] ~~When collecting repositories, dedup by .git abs path, so
worktrees can share a repo at the project level~~ dedup repos at the
repository selector layer, with repos coming from larger worktrees being
preferred
- [x] Open single-file worktree with diff when activating a path not in
the worktree

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-03-06 17:55:28 -05:00 committed by GitHub
parent 330e799293
commit 1763dd714b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 724 additions and 184 deletions

View file

@ -58,7 +58,7 @@ use std::{
future::Future,
mem::{self},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
path::{Component, Path, PathBuf},
pin::Pin,
sync::{
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
@ -212,7 +212,11 @@ impl RepositoryEntry {
self.work_directory.relativize(path)
}
pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
self.work_directory.try_unrelativize(path)
}
pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
self.work_directory.unrelativize(path)
}
@ -491,7 +495,7 @@ impl WorkDirectory {
}
/// This is the opposite operation to `relativize` above
pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
match self {
WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
WorkDirectory::AboveProject {
@ -504,6 +508,33 @@ impl WorkDirectory {
}
}
pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {
match self {
WorkDirectory::InProject { relative_path } => relative_path.join(path).into(),
WorkDirectory::AboveProject {
location_in_repo, ..
} => {
if &path.0 == location_in_repo {
// Single-file worktree
return location_in_repo
.file_name()
.map(Path::new)
.unwrap_or(Path::new(""))
.into();
}
let mut location_in_repo = &**location_in_repo;
let mut parents = PathBuf::new();
loop {
if let Ok(segment) = path.strip_prefix(location_in_repo) {
return parents.join(segment).into();
}
location_in_repo = location_in_repo.parent().unwrap_or(Path::new(""));
parents.push(Component::ParentDir);
}
}
}
}
pub fn display_name(&self) -> String {
match self {
WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
@ -1422,6 +1453,19 @@ impl Worktree {
worktree_scan_id: scan_id as u64,
})
}
pub fn dot_git_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
let mut path = match work_directory {
WorkDirectory::InProject { relative_path } => self.abs_path().join(relative_path),
WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
};
path.push(".git");
path
}
pub fn is_single_file(&self) -> bool {
self.root_dir().is_none()
}
}
impl LocalWorktree {
@ -5509,7 +5553,7 @@ impl BackgroundScanner {
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
let project_path = repository.work_directory.unrelativize(repo_path);
let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {

View file

@ -3412,6 +3412,43 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_unrelativize() {
let work_directory = WorkDirectory::in_project("");
pretty_assertions::assert_eq!(
work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
Some(Path::new("crates/gpui/gpui.rs").into())
);
let work_directory = WorkDirectory::in_project("vendor/some-submodule");
pretty_assertions::assert_eq!(
work_directory.try_unrelativize(&"src/thing.c".into()),
Some(Path::new("vendor/some-submodule/src/thing.c").into())
);
let work_directory = WorkDirectory::AboveProject {
absolute_path: Path::new("/projects/zed").into(),
location_in_repo: Path::new("crates/gpui").into(),
};
pretty_assertions::assert_eq!(
work_directory.try_unrelativize(&"crates/util/util.rs".into()),
None,
);
pretty_assertions::assert_eq!(
work_directory.unrelativize(&"crates/util/util.rs".into()),
Path::new("../util/util.rs").into()
);
pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
pretty_assertions::assert_eq!(
work_directory.unrelativize(&"README.md".into()),
Path::new("../../README.md").into()
);
}
#[track_caller]
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
let mut traversal = snapshot