From e7b3b8bf03f5f8693f8c330b98b656b16ab8f931 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 | 321 +++++++++++++++++++++++---------- crates/zed/src/zed.rs | 6 +- 4 files changed, 233 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2bcec6ee0..fb26340d0f 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 fc5f777da2..7af228bcdb 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 cd122be056..f76d8b2bda 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(); - - // 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); - }); - - 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(); - - 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 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) + }); + + commit_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + }); + + 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(); + + 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 0344c2e82d..c1812d3f08 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); }); })?;