Move repository state RPC handlers to the GitStore (#27391)

This is another in the series of PRs to make the GitStore own all
repository state and enable better concurrency control for git
repository scans.

After this PR, the `RepositoryEntry`s stored in worktree snapshots are
used only as a staging ground for local GitStores to pull from after
git-related events; non-local worktrees don't store them at all,
although this is not reflected in the types. GitTraversal and other
places that need information about repositories get it from the
GitStore. The GitStore also takes over handling of the new
UpdateRepository and RemoveRepository messages. However, repositories
are still discovered and scanned on a per-worktree basis, and we're
still identifying them by the (worktree-specific) project entry ID of
their working directory.

- [x] Remove WorkDirectory from RepositoryEntry
- [x] Remove worktree IDs from repository-related RPC messages
- [x] Handle UpdateRepository and RemoveRepository RPCs from the
GitStore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Cole Miller 2025-03-26 18:23:44 -04:00 committed by GitHub
parent 1e8b50f471
commit 6924720b35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1387 additions and 1399 deletions

View file

@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal;
use crate::git_panel_settings::StatusStyle;
use crate::project_diff::Diff;
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::repository_selector::filtered_repository_entries;
use crate::{branch_picker, render_remote_button};
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
@ -63,7 +63,7 @@ use ui::{
Tooltip,
};
use util::{maybe, post_inc, ResultExt, TryFutureExt};
use workspace::{AppState, OpenOptions, OpenVisible};
use workspace::AppState;
use notifications::status_toast::{StatusToast, ToastIcon};
use workspace::{
@ -195,7 +195,6 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) repo_path: RepoPath,
pub(crate) worktree_path: Arc<Path>,
pub(crate) abs_path: PathBuf,
pub(crate) status: FileStatus,
pub(crate) staging: StageStatus,
@ -203,14 +202,14 @@ pub struct GitStatusEntry {
impl GitStatusEntry {
fn display_name(&self) -> String {
self.worktree_path
self.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
.unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
}
fn parent_dir(&self) -> Option<String> {
self.worktree_path
self.repo_path
.parent()
.map(|parent| parent.to_string_lossy().into_owned())
}
@ -652,7 +651,7 @@ impl GitPanel {
let Some(git_repo) = self.active_repository.as_ref() else {
return;
};
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
return;
};
let Some(ix) = self.entry_by_path(&repo_path) else {
@ -865,7 +864,7 @@ impl GitPanel {
if Some(&entry.repo_path)
== git_repo
.read(cx)
.project_path_to_repo_path(&project_path)
.project_path_to_repo_path(&project_path, cx)
.as_ref()
{
project_diff.focus_handle(cx).focus(window);
@ -875,31 +874,12 @@ impl GitPanel {
}
};
if entry.worktree_path.starts_with("..") {
self.workspace
.update(cx, |workspace, cx| {
workspace
.open_abs_path(
entry.abs_path.clone(),
OpenOptions {
visible: Some(OpenVisible::All),
focus: Some(false),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
})
.ok();
} else {
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
})
.ok();
self.focus_handle.focus(window);
}
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
})
.ok();
self.focus_handle.focus(window);
Some(())
});
@ -916,7 +896,7 @@ impl GitPanel {
let active_repo = self.active_repository.as_ref()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
if entry.status.is_deleted() {
return None;
}
@ -992,7 +972,7 @@ impl GitPanel {
let active_repo = self.active_repository.clone()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
let workspace = self.workspace.clone();
if entry.status.staging().has_staged() {
@ -1052,7 +1032,7 @@ impl GitPanel {
.filter_map(|entry| {
let path = active_repository
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
Some(project.open_buffer(path, cx))
})
.collect()
@ -1218,7 +1198,7 @@ impl GitPanel {
workspace.project().update(cx, |project, cx| {
let project_path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path)?;
.repo_path_to_project_path(&entry.repo_path, cx)?;
project.delete_file(project_path, true, cx)
})
})
@ -2295,16 +2275,12 @@ impl GitPanel {
continue;
}
// dot_git_abs path always has at least one component, namely .git.
let abs_path = repo
.dot_git_abs_path
.parent()
.unwrap()
.join(&entry.repo_path);
let worktree_path = repo.repository_entry.unrelativize(&entry.repo_path);
.repository_entry
.work_directory_abs_path
.join(&entry.repo_path.0);
let entry = GitStatusEntry {
repo_path: entry.repo_path.clone(),
worktree_path,
abs_path,
status: entry.status,
staging,
@ -2883,7 +2859,6 @@ impl GitPanel {
) -> Option<impl IntoElement> {
let active_repository = self.active_repository.clone()?;
let (can_commit, tooltip) = self.configure_commit_button(cx);
let project = self.project.clone().read(cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let enable_coauthors = self.render_co_authors(cx);
@ -2907,7 +2882,7 @@ impl GitPanel {
let display_name = SharedString::from(Arc::from(
active_repository
.read(cx)
.display_name(project, cx)
.display_name()
.trim_end_matches("/"),
));
let editor_is_long = self.commit_editor.update(cx, |editor, cx| {
@ -3236,7 +3211,8 @@ impl GitPanel {
cx: &App,
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
let project_path = (file.worktree_id(cx), file.path()).into();
let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
let ix = self.entry_by_path(&repo_path)?;
let entry = self.entries.get(ix)?;
@ -4056,9 +4032,7 @@ impl RenderOnce for PanelRepoFooter {
let single_repo = project
.as_ref()
.map(|project| {
filtered_repository_entries(project.read(cx).git_store().read(cx), cx).len() == 1
})
.map(|project| project.read(cx).git_store().read(cx).repositories().len() == 1)
.unwrap_or(true);
const MAX_BRANCH_LEN: usize = 16;
@ -4558,66 +4532,65 @@ mod tests {
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
worktree_path: Path::new("gpui.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
worktree_path: Path::new("../util/util.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),
],
);
cx.update_window_entity(&panel, |panel, window, cx| {
panel.select_last(&Default::default(), window, cx);
assert_eq!(panel.selected_entry, Some(2));
panel.open_diff(&Default::default(), window, cx);
});
cx.run_until_parked();
// TODO(cole) restore this once repository deduplication is implemented properly.
//cx.update_window_entity(&panel, |panel, window, cx| {
// panel.select_last(&Default::default(), window, cx);
// assert_eq!(panel.selected_entry, Some(2));
// panel.open_diff(&Default::default(), window, cx);
//});
//cx.run_until_parked();
let worktree_roots = workspace.update(cx, |workspace, cx| {
workspace
.worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(
worktree_roots,
vec![
Path::new(path!("/root/zed/crates/gpui")).into(),
Path::new(path!("/root/zed/crates/util/util.rs")).into(),
]
);
//let worktree_roots = workspace.update(cx, |workspace, cx| {
// workspace
// .worktrees(cx)
// .map(|worktree| worktree.read(cx).abs_path())
// .collect::<Vec<_>>()
//});
//pretty_assertions::assert_eq!(
// worktree_roots,
// vec![
// Path::new(path!("/root/zed/crates/gpui")).into(),
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
// ]
//);
project.update(cx, |project, cx| {
let git_store = project.git_store().read(cx);
// The repo that comes from the single-file worktree can't be selected through the UI.
let filtered_entries = filtered_repository_entries(git_store, cx)
.iter()
.map(|repo| repo.read(cx).worktree_abs_path.clone())
.collect::<Vec<_>>();
assert_eq!(
filtered_entries,
[Path::new(path!("/root/zed/crates/gpui")).into()]
);
// But we can select it artificially here.
let repo_from_single_file_worktree = git_store
.repositories()
.values()
.find(|repo| {
repo.read(cx).worktree_abs_path.as_ref()
== Path::new(path!("/root/zed/crates/util/util.rs"))
})
.unwrap()
.clone();
//project.update(cx, |project, cx| {
// let git_store = project.git_store().read(cx);
// // The repo that comes from the single-file worktree can't be selected through the UI.
// let filtered_entries = filtered_repository_entries(git_store, cx)
// .iter()
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
// .collect::<Vec<_>>();
// assert_eq!(
// filtered_entries,
// [Path::new(path!("/root/zed/crates/gpui")).into()]
// );
// // But we can select it artificially here.
// let repo_from_single_file_worktree = git_store
// .repositories()
// .values()
// .find(|repo| {
// repo.read(cx).worktree_abs_path.as_ref()
// == Path::new(path!("/root/zed/crates/util/util.rs"))
// })
// .unwrap()
// .clone();
// Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
});
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
//});
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@ -4634,14 +4607,12 @@ mod tests {
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
worktree_path: Path::new("../../gpui/gpui.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
GitListEntry::GitStatusEntry(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
worktree_path: Path::new("util.rs").into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
},),

View file

@ -343,7 +343,8 @@ impl ProjectDiff {
if !entry.status.has_changes() {
continue;
}
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path, cx)
else {
continue;
};
let namespace = if repo.has_conflict(&entry.repo_path) {

View file

@ -3,10 +3,7 @@ use gpui::{
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{
git_store::{GitStore, Repository},
Project,
};
use project::{git_store::Repository, Project};
use std::sync::Arc;
use ui::{prelude::*, ListItem, ListItemSpacing};
use workspace::{ModalView, Workspace};
@ -40,21 +37,23 @@ impl RepositorySelector {
cx: &mut Context<Self>,
) -> Self {
let git_store = project_handle.read(cx).git_store().clone();
let repository_entries = git_store.update(cx, |git_store, cx| {
filtered_repository_entries(git_store, cx)
let repository_entries = git_store.update(cx, |git_store, _cx| {
git_store
.repositories()
.values()
.cloned()
.collect::<Vec<_>>()
});
let project = project_handle.read(cx);
let filtered_repositories = repository_entries.clone();
let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
a.read(cx)
.display_name(project, cx)
.display_name()
.len()
.cmp(&b.read(cx).display_name(project, cx).len())
.cmp(&b.read(cx).display_name().len())
});
let delegate = RepositorySelectorDelegate {
project: project_handle.downgrade(),
repository_selector: cx.entity().downgrade(),
repository_entries,
filtered_repositories,
@ -71,36 +70,36 @@ impl RepositorySelector {
}
}
pub(crate) fn filtered_repository_entries(
git_store: &GitStore,
cx: &App,
) -> Vec<Entity<Repository>> {
let repositories = git_store
.repositories()
.values()
.sorted_by_key(|repo| {
let repo = repo.read(cx);
(
repo.dot_git_abs_path.clone(),
repo.worktree_abs_path.clone(),
)
})
.collect::<Vec<&Entity<Repository>>>();
repositories
.chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
.flat_map(|chunk| {
let has_non_single_file_worktree = chunk
.iter()
.any(|repo| !repo.read(cx).is_from_single_file_worktree);
chunk.iter().filter(move |repo| {
// Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
!repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
})
})
.map(|&repo| repo.clone())
.collect()
}
//pub(crate) fn filtered_repository_entries(
// git_store: &GitStore,
// cx: &App,
//) -> Vec<Entity<Repository>> {
// let repositories = git_store
// .repositories()
// .values()
// .sorted_by_key(|repo| {
// let repo = repo.read(cx);
// (
// repo.dot_git_abs_path.clone(),
// repo.worktree_abs_path.clone(),
// )
// })
// .collect::<Vec<&Entity<Repository>>>();
//
// repositories
// .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
// .flat_map(|chunk| {
// let has_non_single_file_worktree = chunk
// .iter()
// .any(|repo| !repo.read(cx).is_from_single_file_worktree);
// chunk.iter().filter(move |repo| {
// // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
// !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
// })
// })
// .map(|&repo| repo.clone())
// .collect()
//}
impl EventEmitter<DismissEvent> for RepositorySelector {}
@ -119,7 +118,6 @@ impl Render for RepositorySelector {
impl ModalView for RepositorySelector {}
pub struct RepositorySelectorDelegate {
project: WeakEntity<Project>,
repository_selector: WeakEntity<RepositorySelector>,
repository_entries: Vec<Entity<Repository>>,
filtered_repositories: Vec<Entity<Repository>>,
@ -225,9 +223,8 @@ impl PickerDelegate for RepositorySelectorDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let project = self.project.upgrade()?;
let repo_info = self.filtered_repositories.get(ix)?;
let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
let display_name = repo_info.read(cx).display_name();
Some(
ListItem::new(ix)
.inset(true)