From 8d9d14c2b9df6ac675ae684ed92af386de260193 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 3 Mar 2025 18:32:03 -0500 Subject: [PATCH] git: Use worktree paths in the panel (#25950) This PR changes the git panel to use worktree-relative paths for its entries, instead of repository-relative paths as before. Paths that lie outside the active repository's worktree are no longer shown in the panel. Note that in both respects this is how the project diff editor already works, so this PR brings those two pieces of UI into harmony. Release Notes: - N/A --- Cargo.lock | 1 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 313 +++++++++++++++++++++++---------- crates/zed/src/zed.rs | 6 +- 4 files changed, 229 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ab7233c177c1f11657c538ef94b394c1ead24c4..bb246ecde08e7590472d718fc9a6049bc660e72b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5439,6 +5439,7 @@ dependencies = [ "panel", "picker", "postage", + "pretty_assertions", "project", "schemars", "serde", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index fc5f777da2c64a13bec3c6616314b07045851536..7af228bcdb8c680736be7b6ea2ed0e38054ae64d 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -61,6 +61,7 @@ ctor.workspace = true env_logger.workspace = true editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } unindent.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index cd122be0560a1e1a8e5a2b5b2217bc5f4d3920a8..f76d8b2bda6818ca517154f24ef1e8b4b8fe030c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -18,7 +18,14 @@ use git::repository::{ }; use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged}; use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; -use gpui::*; +use gpui::{ + actions, anchored, deferred, hsla, percentage, point, uniform_list, Action, Animation, + AnimationExt as _, AnyView, BoxShadow, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, PromptLevel, + ScrollStrategy, Stateful, Subscription, Task, Transformation, UniformListScrollHandle, + WeakEntity, +}; use itertools::Itertools; use language::{Buffer, File}; use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; @@ -35,6 +42,7 @@ use settings::Settings as _; use smallvec::smallvec; use std::cell::RefCell; use std::future::Future; +use std::path::Path; use std::rc::Rc; use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; @@ -44,6 +52,7 @@ use ui::{ Scrollbar, ScrollbarState, Tooltip, }; use util::{maybe, post_inc, ResultExt, TryFutureExt}; +use workspace::AppState; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -63,7 +72,12 @@ actions!( ] ); -fn prompt(msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App) -> Task> +fn prompt( + msg: &str, + detail: Option<&str>, + window: &mut Window, + cx: &mut App, +) -> Task> where T: IntoEnumIterator + VariantNames + 'static, { @@ -165,6 +179,7 @@ impl GitListEntry { #[derive(Debug, PartialEq, Eq, Clone)] pub struct GitStatusEntry { pub(crate) repo_path: RepoPath, + pub(crate) worktree_path: Arc, pub(crate) status: FileStatus, pub(crate) is_staged: Option, } @@ -262,96 +277,94 @@ pub(crate) fn commit_message_editor( impl GitPanel { pub fn new( - workspace: &mut Workspace, + workspace: Entity, + project: Entity, + app_state: Arc, window: &mut Window, - cx: &mut Context, - ) -> Entity { - let fs = workspace.app_state().fs.clone(); - let project = workspace.project().clone(); + cx: &mut Context, + ) -> Self { + let fs = app_state.fs.clone(); let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); - let workspace = cx.entity().downgrade(); + let workspace = workspace.downgrade(); - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { - this.hide_scrollbar(window, cx); - }) - .detach(); + let focus_handle = cx.focus_handle(); + cx.on_focus(&focus_handle, window, Self::focus_in).detach(); + cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { + this.hide_scrollbar(window, cx); + }) + .detach(); - // just to let us render a placeholder editor. - // Once the active git repo is set, this buffer will be replaced. - let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); - let commit_editor = cx.new(|cx| { - commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) - }); + // just to let us render a placeholder editor. + // Once the active git repo is set, this buffer will be replaced. + let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); + let commit_editor = cx.new(|cx| { + commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx) + }); - commit_editor.update(cx, |editor, cx| { - editor.clear(window, cx); - }); + commit_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + }); - let scroll_handle = UniformListScrollHandle::new(); + let scroll_handle = UniformListScrollHandle::new(); - cx.subscribe_in( - &git_store, - window, - move |this, git_store, event, window, cx| match event { - GitEvent::FileSystemUpdated => { - this.schedule_update(false, window, cx); - } - GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => { - this.active_repository = git_store.read(cx).active_repository(); - this.schedule_update(true, window, cx); - } - }, - ) - .detach(); + cx.subscribe_in( + &git_store, + window, + move |this, git_store, event, window, cx| match event { + GitEvent::FileSystemUpdated => { + this.schedule_update(false, window, cx); + } + GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => { + this.active_repository = git_store.read(cx).active_repository(); + this.schedule_update(true, window, cx); + } + }, + ) + .detach(); - let scrollbar_state = - ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); - - let repository_selector = - cx.new(|cx| RepositorySelector::new(project.clone(), window, cx)); - - let mut git_panel = Self { - pending_remote_operations: Default::default(), - remote_operation_id: 0, - active_repository, - commit_editor, - suggested_commit_message: None, - conflicted_count: 0, - conflicted_staged_count: 0, - current_modifiers: window.modifiers(), - add_coauthors: true, - entries: Vec::new(), - focus_handle: cx.focus_handle(), - fs, - hide_scrollbar_task: None, - new_count: 0, - new_staged_count: 0, - pending: Vec::new(), - pending_commit: None, - pending_serialization: Task::ready(None), - project, - repository_selector, - scroll_handle, - scrollbar_state, - selected_entry: None, - marked_entries: Vec::new(), - show_scrollbar: false, - tracked_count: 0, - tracked_staged_count: 0, - update_visible_entries_task: Task::ready(()), - width: Some(px(360.)), - context_menu: None, - workspace, - modal_open: false, - }; - git_panel.schedule_update(false, window, cx); - git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); - git_panel - }) + let scrollbar_state = + ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); + + let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx)); + + let mut git_panel = Self { + pending_remote_operations: Default::default(), + remote_operation_id: 0, + active_repository, + commit_editor, + suggested_commit_message: None, + conflicted_count: 0, + conflicted_staged_count: 0, + current_modifiers: window.modifiers(), + add_coauthors: true, + entries: Vec::new(), + focus_handle: cx.focus_handle(), + fs, + hide_scrollbar_task: None, + new_count: 0, + new_staged_count: 0, + pending: Vec::new(), + pending_commit: None, + pending_serialization: Task::ready(None), + project, + repository_selector, + scroll_handle, + scrollbar_state, + selected_entry: None, + marked_entries: Vec::new(), + show_scrollbar: false, + tracked_count: 0, + tracked_staged_count: 0, + update_visible_entries_task: Task::ready(()), + width: Some(px(360.)), + context_menu: None, + workspace, + modal_open: false, + }; + git_panel.schedule_update(false, window, cx); + git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); + git_panel } pub fn entry_by_path(&self, path: &RepoPath) -> Option { @@ -1457,7 +1470,7 @@ impl GitPanel { &mut self, window: &mut Window, cx: &mut Context, - ) -> impl Future>> { + ) -> impl Future>> { let repo = self.active_repository.clone(); let workspace = self.workspace.clone(); let mut cx = window.to_async(cx); @@ -1694,10 +1707,8 @@ impl GitPanel { return; }; - // First pass - collect all paths let repo = repo.read(cx); - // Second pass - create entries with proper depth calculation for entry in repo.status() { let is_conflict = repo.has_conflict(&entry.repo_path); let is_new = entry.status.is_created(); @@ -1711,8 +1722,12 @@ impl GitPanel { continue; } + let Some(worktree_path) = repo.repository_entry.unrelativize(&entry.repo_path) else { + continue; + }; let entry = GitStatusEntry { repo_path: entry.repo_path.clone(), + worktree_path, status: entry.status, is_staged, }; @@ -2363,7 +2378,7 @@ impl GitPanel { &self, sha: &str, cx: &mut Context, - ) -> Task> { + ) -> Task> { let Some(repo) = self.active_repository.clone() else { return Task::ready(Err(anyhow::anyhow!("no active repo"))); }; @@ -2448,12 +2463,12 @@ impl GitPanel { cx: &Context, ) -> AnyElement { let display_name = entry - .repo_path + .worktree_path .file_name() .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned()); + .unwrap_or_else(|| entry.worktree_path.to_string_lossy().into_owned()); - let repo_path = entry.repo_path.clone(); + let worktree_path = entry.worktree_path.clone(); let selected = self.selected_entry == Some(ix); let marked = self.marked_entries.contains(&ix); let status_style = GitPanelSettings::get_global(cx).status_style; @@ -2619,7 +2634,7 @@ impl GitPanel { h_flex() .items_center() .overflow_hidden() - .when_some(repo_path.parent(), |this, parent| { + .when_some(worktree_path.parent(), |this, parent| { let parent_str = parent.to_string_lossy(); if !parent_str.is_empty() { this.child( @@ -3667,3 +3682,119 @@ impl ComponentPreview for PanelRepoFooter { .into_any_element() } } + +#[cfg(test)] +mod tests { + use git::status::StatusCode; + use gpui::TestAppContext; + use project::{FakeFs, WorktreeSettings}; + use serde_json::json; + use settings::SettingsStore; + use theme::LoadThemes; + use util::path; + + use super::*; + + fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + WorktreeSettings::register(cx); + workspace::init_settings(cx); + theme::init(LoadThemes::JustBase, cx); + language::init(cx); + editor::init(cx); + Project::init_settings(cx); + crate::init(cx); + }); + } + + #[gpui::test] + async fn test_entry_worktree_paths(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "zed": { + ".git": {}, + "crates": { + "gpui": { + "gpui.rs": "fn main() {}" + }, + "util": { + "util.rs": "fn do_it() {}" + } + } + }, + }), + ) + .await; + + fs.set_status_for_repo_via_git_operation( + Path::new("/root/zed/.git"), + &[ + ( + Path::new("crates/gpui/gpui.rs"), + StatusCode::Modified.worktree(), + ), + ( + Path::new("crates/util/util.rs"), + StatusCode::Modified.worktree(), + ), + ], + ); + + let project = + Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + cx.read(|cx| { + project + .read(cx) + .worktrees(cx) + .nth(0) + .unwrap() + .read(cx) + .as_local() + .unwrap() + .scan_complete() + }) + .await; + + cx.executor().run_until_parked(); + + let app_state = workspace.update(cx, |workspace, _| workspace.app_state().clone()); + let panel = cx.new_window_entity(|window, cx| { + GitPanel::new(workspace, project, app_state, window, cx) + }); + + let handle = cx.update_window_entity(&panel, |panel, window, cx| { + panel.schedule_update(false, window, cx); + std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) + }); + cx.executor().advance_clock(2 * UPDATE_DEBOUNCE); + handle.await; + + let entries = panel.update(cx, |panel, _| panel.entries.clone()); + pretty_assertions::assert_eq!( + entries, + [ + GitListEntry::Header(GitHeaderEntry { + header: Section::Tracked + }), + GitListEntry::GitStatusEntry(GitStatusEntry { + repo_path: "crates/gpui/gpui.rs".into(), + worktree_path: Path::new("gpui.rs").into(), + status: StatusCode::Modified.worktree(), + is_staged: Some(false), + }) + ], + ) + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0344c2e82daf2115ace29d9a3581bbd621f5c059..c1812d3f08d11d947203f56c90b9fb17b6306834 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -22,6 +22,7 @@ use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag}; use futures::{channel::mpsc, select_biased, StreamExt}; +use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, @@ -429,7 +430,10 @@ fn initialize_panels( workspace.add_panel(chat_panel, window, cx); workspace.add_panel(notification_panel, window, cx); cx.when_flag_enabled::(window, |workspace, window, cx| { - let git_panel = git_ui::git_panel::GitPanel::new(workspace, window, cx); + let entity = cx.entity(); + let project = workspace.project().clone(); + let app_state = workspace.app_state().clone(); + let git_panel = cx.new(|cx| GitPanel::new(entity, project, app_state, window, cx)); workspace.add_panel(git_panel, window, cx); }); })?;