diff --git a/Cargo.lock b/Cargo.lock index d84cdc4cb2..1ec44169d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3373,7 +3373,6 @@ dependencies = [ "fs", "futures 0.3.32", "git", - "git_graph", "git_hosting_providers", "git_ui", "gpui", @@ -7279,40 +7278,6 @@ dependencies = [ "ztracing", ] -[[package]] -name = "git_graph" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-channel 2.5.0", - "collections", - "db", - "editor", - "fs", - "git", - "git_ui", - "gpui", - "language", - "language_model", - "menu", - "picker", - "project", - "project_panel", - "rand 0.9.4", - "release_channel", - "remote_connection", - "search", - "serde_json", - "settings", - "smallvec", - "task", - "theme", - "theme_settings", - "time", - "ui", - "workspace", -] - [[package]] name = "git_hosting_providers" version = "0.1.0" @@ -7342,6 +7307,7 @@ dependencies = [ "agent_settings", "anyhow", "askpass", + "async-channel 2.5.0", "buffer_diff", "call", "collections", @@ -7374,9 +7340,11 @@ dependencies = [ "prompt_store", "proto", "rand 0.9.4", + "release_channel", "remote", "remote_connection", "schemars 1.0.4", + "search", "serde", "serde_json", "settings", @@ -22887,7 +22855,6 @@ dependencies = [ "fs", "futures 0.3.32", "git", - "git_graph", "git_hosting_providers", "git_ui", "go_to_line", diff --git a/Cargo.toml b/Cargo.toml index a293adfb82..2d448c1b6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,6 @@ members = [ "crates/fuzzy", "crates/fuzzy_nucleo", "crates/git", - "crates/git_graph", "crates/git_hosting_providers", "crates/git_ui", "crates/go_to_line", @@ -339,7 +338,6 @@ fs = { path = "crates/fs" } fuzzy = { path = "crates/fuzzy" } fuzzy_nucleo = { path = "crates/fuzzy_nucleo" } git = { path = "crates/git" } -git_graph = { path = "crates/git_graph" } git_hosting_providers = { path = "crates/git_hosting_providers" } git_ui = { path = "crates/git_ui" } go_to_line = { path = "crates/go_to_line" } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 67403f7b59..9ad0949e15 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -97,7 +97,6 @@ extension.workspace = true file_finder.workspace = true fs = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } -git_graph = { workspace = true, features = ["test-support"] } git_hosting_providers.workspace = true git_ui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index 26faeb7c5f..d5f71085d5 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -11,7 +11,7 @@ use git::{ repository::{CommitData, InitialGraphCommitData, RepoPath, Worktree as GitWorktree}, status::{DiffStat, FileStatus, StatusCode, TrackedStatus}, }; -use git_graph::GitGraph; +use git_ui::git_graph::GitGraph; use git_ui::{git_panel::GitPanel, project_diff::ProjectDiff}; use gpui::{ AppContext as _, BackgroundExecutor, Entity, IntoElement as _, SharedString, TestAppContext, @@ -763,11 +763,9 @@ async fn test_remote_git_graph_data_and_search( .await; cx_a.update(|cx| { git_ui::init(cx); - git_graph::init(cx); }); cx_b.update(|cx| { git_ui::init(cx); - git_graph::init(cx); }); let active_call_a = cx_a.read(ActiveCall::global); @@ -781,7 +779,7 @@ async fn test_remote_git_graph_data_and_search( let search_query = "graph search match"; let mut rng = StdRng::seed_from_u64(7); - let commits = git_graph::generate_random_commit_dag(&mut rng, 12, true); + let commits = git_ui::git_graph::generate_random_commit_dag(&mut rng, 12, true); let dot_git = Path::new(path!("/project/.git")); client_a.fs().set_graph_commits(dot_git, commits.clone()); diff --git a/crates/git_graph/Cargo.toml b/crates/git_graph/Cargo.toml deleted file mode 100644 index 2922453c46..0000000000 --- a/crates/git_graph/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "git_graph" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/git_graph.rs" - -[features] -default = [] -test-support = [ - "dep:rand", - "project/test-support", - "gpui/test-support", - "remote_connection/test-support", -] - -[dependencies] -anyhow.workspace = true -async-channel.workspace = true -collections.workspace = true -db.workspace = true -editor.workspace = true -git.workspace = true -git_ui.workspace = true -gpui.workspace = true -language.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -project_panel.workspace = true -rand = { workspace = true, optional = true } -release_channel.workspace = true -search.workspace = true -settings.workspace = true -smallvec.workspace = true -task.workspace = true -theme.workspace = true -theme_settings.workspace = true -time.workspace = true -ui.workspace = true -workspace.workspace = true - -[dev-dependencies] -db = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -git = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language_model.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -remote_connection = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/git_graph/LICENSE-GPL b/crates/git_graph/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/git_graph/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 3273277c79..829ba4f9d8 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -19,6 +19,7 @@ test-support = ["multi_buffer/test-support", "remote_connection/test-support"] agent_settings.workspace = true anyhow.workspace = true askpass.workspace = true +async-channel.workspace = true buffer_diff.workspace = true call.workspace = true collections.workspace = true @@ -48,15 +49,18 @@ project.workspace = true prompt_store.workspace = true proto.workspace = true rand.workspace = true +release_channel.workspace = true remote_connection.workspace = true remote.workspace = true schemars.workspace = true +search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smallvec.workspace = true strum.workspace = true sysinfo.workspace = true +task.workspace = true telemetry.workspace = true terminal.workspace = true theme.workspace = true @@ -78,7 +82,10 @@ windows.workspace = true [dev-dependencies] ctor.workspace = true +db = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } +fs = { workspace = true, features = ["test-support"] } +git = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } indoc.workspace = true pretty_assertions.workspace = true diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 247c98c569..cb3ae8b348 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -1306,7 +1306,7 @@ impl Render for CommitViewToolbar { .tooltip(Tooltip::text("Show in Git Graph")) .on_click(move |_, window, cx| { window.dispatch_action( - Box::new(crate::git_panel::OpenAtCommit { + Box::new(crate::git_graph::OpenAtCommit { sha: sha_for_graph.clone(), }), cx, diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_ui/src/git_graph.rs similarity index 97% rename from crates/git_graph/src/git_graph.rs rename to crates/git_ui/src/git_graph.rs index e176a34e2b..62685586bf 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_ui/src/git_graph.rs @@ -1,3 +1,8 @@ +use crate::{ + commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip}, + commit_view::CommitView, + git_status_icon, +}; use collections::{BTreeMap, HashMap, IndexSet}; use editor::Editor; use git::{ @@ -10,11 +15,6 @@ use git::{ }, status::{FileStatus, StatusCode, TrackedStatus}, }; -use git_ui::{ - commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip}, - commit_view::CommitView, - git_status_icon, -}; use gpui::{ Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, @@ -33,7 +33,6 @@ use project::{ RepositoryEvent, RepositoryId, }, }; -use project_panel::ProjectPanel; use search::{ SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, buffer_search, @@ -340,24 +339,24 @@ struct SearchState { case_sensitive: bool, editor: Entity, state: QueryState, - pub matches: IndexSet, - pub selected_index: Option, + matches: IndexSet, + selected_index: Option, } -pub struct SplitState { +struct SplitState { left_ratio: f32, visible_left_ratio: f32, } impl SplitState { - pub fn new() -> Self { + fn new() -> Self { Self { left_ratio: 1.0, visible_left_ratio: 1.0, } } - pub fn right_ratio(&self) -> f32 { + fn right_ratio(&self) -> f32 { 1.0 - self.visible_left_ratio } @@ -391,6 +390,8 @@ impl SplitState { actions!( git_graph, [ + /// Opens the Git Graph Tab. + Open, /// Copies the SHA of the selected commit to the clipboard. CopyCommitSha, /// Copies a tag from the selected commit to the clipboard. @@ -410,6 +411,13 @@ actions!( ] ); +/// Opens the Git Graph Tab at a specific commit. +#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)] +#[action(namespace = git_graph)] +pub struct OpenAtCommit { + pub sha: String, +} + fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] { static FORMAT: OnceLock>> = OnceLock::new(); FORMAT.get_or_init(|| { @@ -893,7 +901,7 @@ pub fn init(cx: &mut App) { div.on_action({ let workspace = workspace.clone(); - move |_: &git_ui::git_panel::Open, window, cx| { + move |_: &Open, window, cx| { workspace .update(cx, |workspace, cx| { let Some(repo) = @@ -918,33 +926,29 @@ pub fn init(cx: &mut App) { .ok(); } }) - .on_action( - move |action: &git_ui::git_panel::OpenAtCommit, window, cx| { - let sha = action.sha.clone(); - workspace - .update(cx, |workspace, cx| { - let Some(repo) = - workspace.project().read(cx).active_repository(cx) - else { - return; - }; - let selected_repo_id = repo.read(cx).id; + .on_action(move |action: &OpenAtCommit, window, cx| { + let sha = action.sha.clone(); + workspace + .update(cx, |workspace, cx| { + let Some(repo) = workspace.project().read(cx).active_repository(cx) + else { + return; + }; + let selected_repo_id = repo.read(cx).id; - let git_store = - workspace.project().read(cx).git_store().clone(); - open_or_reuse_graph( - workspace, - selected_repo_id, - git_store, - LogSource::All, - Some(sha), - window, - cx, - ); - }) - .ok(); - }, - ) + let git_store = workspace.project().read(cx).git_store().clone(); + open_or_reuse_graph( + workspace, + selected_repo_id, + git_store, + LogSource::All, + Some(sha), + window, + cx, + ); + }) + .ok(); + }) }, ) }); @@ -952,28 +956,32 @@ pub fn init(cx: &mut App) { .detach(); } +/// Resolves a `git::FileHistory` target from a known project path (used by +/// callers like `project_panel` that own a focused selection but cannot be +/// referenced from this module due to dependency direction). +pub fn resolve_file_history_target_from_project_path( + workspace: &Workspace, + project_path: &ProjectPath, + cx: &App, +) -> Option<(RepositoryId, LogSource)> { + let git_store = workspace.project().read(cx).git_store(); + let (repo, repo_path) = git_store + .read(cx) + .repository_and_path_for_project_path(project_path, cx)?; + let log_source = if repo_path.is_empty() { + LogSource::All + } else { + LogSource::Path(repo_path) + }; + Some((repo.read(cx).id, log_source)) +} + fn resolve_file_history_target( workspace: &Workspace, window: &Window, cx: &App, ) -> Option<(RepositoryId, LogSource)> { - if let Some(panel) = workspace.panel::(cx) - && panel.read(cx).focus_handle(cx).contains_focused(window, cx) - && let Some(project_path) = panel.read(cx).selected_entry_project_path(cx) - { - let git_store = workspace.project().read(cx).git_store(); - let (repo, repo_path) = git_store - .read(cx) - .repository_and_path_for_project_path(&project_path, cx)?; - let log_source = if repo_path.is_empty() { - LogSource::All - } else { - LogSource::Path(repo_path) - }; - return Some((repo.read(cx).id, log_source)); - } - - if let Some(panel) = workspace.panel::(cx) + if let Some(panel) = workspace.panel::(cx) && panel.read(cx).focus_handle(cx).contains_focused(window, cx) && let Some((repository, repo_path)) = panel.read(cx).selected_file_history_target() { @@ -997,7 +1005,7 @@ fn resolve_file_history_target( Some((repo.read(cx).id, LogSource::Path(repo_path))) } -fn open_or_reuse_graph( +pub fn open_or_reuse_graph( workspace: &mut Workspace, repo_id: RepositoryId, git_store: Entity, @@ -2314,22 +2322,6 @@ impl GitGraph { cx.notify(); } - fn get_remote( - &self, - repository: &Repository, - _window: &mut Window, - cx: &mut App, - ) -> Option { - let remote_url = repository.default_remote_url()?; - let provider_registry = GitHostingProviderRegistry::default_global(cx); - let (provider, parsed) = parse_git_remote_url(provider_registry, &remote_url)?; - Some(GitRemote { - host: provider, - owner: parsed.owner.into(), - repo: parsed.repo.into(), - }) - } - fn render_search_bar(&self, cx: &mut Context) -> impl IntoElement { let color = cx.theme().colors(); let query_focus_handle = self @@ -2536,7 +2528,16 @@ impl GitGraph { }) .unwrap_or_default(); - let remote = repository.update(cx, |repo, cx| self.get_remote(repo, window, cx)); + let remote = repository.update(cx, |repo, cx| { + let remote_url = repo.default_remote_url()?; + let provider_registry = GitHostingProviderRegistry::default_global(cx); + let (provider, parsed) = parse_git_remote_url(provider_registry, &remote_url)?; + Some(GitRemote { + host: provider, + owner: parsed.owner.into(), + repo: parsed.repo.into(), + }) + }); let avatar = { let author_email_for_avatar = if author_email.is_empty() { @@ -2733,7 +2734,7 @@ impl GitGraph { }) .when_some(remote.clone(), |this, remote| { let provider_name = remote.host.name(); - let icon = git_ui::get_provider_icon(provider_name.as_str()); + let icon = crate::get_provider_icon(provider_name.as_str()); let parsed_remote = ParsedGitRemote { owner: remote.owner.as_ref().into(), repo: remote.repo.as_ref().into(), @@ -4084,6 +4085,11 @@ impl GitGraph { .map(|commit| commit.data.clone()) .collect() } + + #[cfg(any(test, feature = "test-support"))] + pub fn log_source_for_test(&self) -> &LogSource { + &self.log_source + } } /// Generates a random commit DAG suitable for testing git graph rendering. @@ -4219,9 +4225,7 @@ mod tests { cx.set_global(settings_store); theme_settings::init(theme::LoadThemes::JustBase, cx); language_model::init(cx); - git_ui::init(cx); - project_panel::init(cx); - init(cx); + crate::init(cx); }); } @@ -4794,24 +4798,14 @@ mod tests { }); repository.update(cx, |repo, cx| { - repo.graph_data( - crate::LogSource::default(), - crate::LogOrder::default(), - 0..usize::MAX, - cx, - ); + repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx); }); cx.run_until_parked(); let graph_commits: Vec> = repository.update(cx, |repo, cx| { - repo.graph_data( - crate::LogSource::default(), - crate::LogOrder::default(), - 0..usize::MAX, - cx, - ) - .commits - .to_vec() + repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx) + .commits + .to_vec() }); let mut graph_data = GraphData::new(8); @@ -4865,12 +4859,7 @@ mod tests { }); repository.update(cx, |repo, cx| { - repo.graph_data( - crate::LogSource::default(), - crate::LogOrder::default(), - 0..usize::MAX, - cx, - ); + repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx); }); project @@ -4888,7 +4877,7 @@ mod tests { "initial repository scan should emit HeadChanged" ); let commit_count_after = repository.read_with(cx, |repo, _| { - repo.get_graph_data(crate::LogSource::default(), crate::LogOrder::default()) + repo.get_graph_data(LogSource::default(), LogOrder::default()) .map(|data| data.commit_data.len()) .unwrap() }); @@ -4927,18 +4916,13 @@ mod tests { }); repository.update(cx, |repo, cx| { - repo.graph_data( - crate::LogSource::default(), - crate::LogOrder::default(), - 0..usize::MAX, - cx, - ); + repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx); }); cx.run_until_parked(); let error = repository.read_with(cx, |repo, _| { - repo.get_graph_data(crate::LogSource::default(), crate::LogOrder::default()) + repo.get_graph_data(LogSource::default(), LogOrder::default()) .and_then(|data| data.error.clone()) }); @@ -5070,14 +5054,12 @@ mod tests { } #[gpui::test] - async fn test_file_history_action_uses_focused_source_and_reuses_matching_graph( - cx: &mut TestAppContext, - ) { + async fn test_file_history_action_uses_git_panel_and_editor_sources(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - Path::new("/project"), + Path::new(util::path!("/project")), json!({ ".git": {}, "tracked1.txt": "tracked 1", @@ -5085,15 +5067,22 @@ mod tests { }), ) .await; + fs.set_status_for_repo( + Path::new(util::path!("/project/.git")), + &[ + ("tracked1.txt", StatusCode::Modified.worktree()), + ("tracked2.txt", StatusCode::Modified.worktree()), + ], + ); let commits = vec![Arc::new(InitialGraphCommitData { sha: Oid::from_bytes(&[1; 20]).unwrap(), parents: smallvec![], ref_names: vec!["HEAD".into(), "refs/heads/main".into()], })]; - fs.set_graph_commits(Path::new("/project/.git"), commits); + fs.set_graph_commits(Path::new(util::path!("/project/.git")), commits); - let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + let project = Project::test(fs.clone(), [Path::new(util::path!("/project"))], cx).await; cx.run_until_parked(); let repository = project.read_with(cx, |project, cx| { @@ -5127,18 +5116,10 @@ mod tests { }) .expect("window should be available"); cx.background_executor.allow_parking(); - let project_panel = cx - .foreground_executor() - .clone() - .block_test(ProjectPanel::load( - weak_workspace.clone(), - async_window_cx.clone(), - )) - .expect("project panel should load"); let git_panel = cx .foreground_executor() .clone() - .block_test(git_ui::git_panel::GitPanel::load( + .block_test(crate::git_panel::GitPanel::load( weak_workspace, async_window_cx, )) @@ -5149,21 +5130,20 @@ mod tests { .update(cx, |multi, window, cx| { let workspace = multi.workspace(); workspace.update(cx, |workspace, cx| { - workspace.add_panel(project_panel.clone(), window, cx); workspace.add_panel(git_panel.clone(), window, cx); }); }) .expect("workspace window should be available"); + cx.executor().advance_clock(Duration::from_millis(100)); cx.run_until_parked(); workspace_window - .update(cx, |multi, window, cx| { - let workspace = multi.workspace(); - project_panel.update(cx, |panel, cx| { - panel.select_path_for_test(tracked1.clone(), cx) + .update(cx, |_, window, cx| { + git_panel.update(cx, |panel, cx| { + panel.select_entry_by_path(tracked1.clone(), window, cx); }); - workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); + git_panel.update(cx, |panel, cx| { + panel.focus_handle(cx).focus(window, cx); }); }) .expect("workspace window should be available"); @@ -5185,13 +5165,12 @@ mod tests { }); workspace_window - .update(cx, |multi, window, cx| { - let workspace = multi.workspace(); + .update(cx, |_, window, cx| { git_panel.update(cx, |panel, cx| { panel.select_entry_by_path(tracked1.clone(), window, cx); }); - workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); + git_panel.update(cx, |panel, cx| { + panel.focus_handle(cx).focus(window, cx); }); }) .expect("workspace window should be available"); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 194b531e91..70a9f7bb56 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -134,21 +134,6 @@ actions!( ] ); -actions!( - git_graph, - [ - /// Opens the Git Graph Tab. - Open, - ] -); - -/// Opens the Git Graph Tab at a specific commit. -#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)] -#[action(namespace = git_graph)] -pub struct OpenAtCommit { - pub sha: String, -} - fn prompt( msg: &str, detail: Option<&str>, @@ -5027,10 +5012,14 @@ impl GitPanel { IconButton::new("git-graph-button", IconName::GitGraph) .icon_size(IconSize::Small) .tooltip(|_window, cx| { - Tooltip::for_action("Open Git Graph", &Open, cx) + Tooltip::for_action( + "Open Git Graph", + &crate::git_graph::Open, + cx, + ) }) .on_click(|_, window, cx| { - window.dispatch_action(Open.boxed_clone(), cx) + window.dispatch_action(crate::git_graph::Open.boxed_clone(), cx) }), ), ), diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index d19ac55206..44eb2a2204 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -36,6 +36,7 @@ pub mod commit_tooltip; pub mod commit_view; mod conflict_view; pub mod file_diff_view; +pub mod git_graph; pub mod git_panel; mod git_panel_settings; pub mod git_picker; @@ -69,6 +70,7 @@ pub fn get_provider_icon(name: &str) -> IconName { pub fn init(cx: &mut App) { editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx); commit_view::init(cx); + git_graph::init(cx); cx.observe_new(|editor: &mut Editor, _, cx| { conflict_view::register_editor(editor, editor.buffer().clone(), cx); diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 62ebe3eb9f..d20b09107d 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -55,6 +55,7 @@ fs.workspace = true client = { workspace = true, features = ["test-support"] } criterion.workspace = true editor = { workspace = true, features = ["test-support"] } +git_ui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } remote_connection = { workspace = true, features = ["test-support"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8cce8ab442..3cd6285258 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -527,6 +527,50 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.delete(action, window, cx)); } }); + + // Forwards `git::FileHistory` to `git_ui::git_graph` when the project + // panel is the focused source of selection. Lives here (and not in + // `git_ui`) so that `git_ui` does not need to depend on + // `project_panel`, which would create a dependency cycle. + workspace.register_action_renderer(|div, workspace, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return div; + }; + if !panel.read(cx).focus_handle(cx).contains_focused(window, cx) { + return div; + } + if panel.read(cx).selected_entry_project_path(cx).is_none() { + return div; + } + let workspace = workspace.weak_handle(); + div.capture_action(move |_: &git::FileHistory, window, cx| { + workspace + .update(cx, |workspace, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(project_path) = panel.read(cx).selected_entry_project_path(cx) + else { + return; + }; + let Some((repo_id, log_source)) = + git_ui::git_graph::resolve_file_history_target_from_project_path( + workspace, + &project_path, + cx, + ) + else { + return; + }; + let git_store = workspace.project().read(cx).git_store().clone(); + git_ui::git_graph::open_or_reuse_graph( + workspace, repo_id, git_store, log_source, None, window, cx, + ); + }) + .log_err(); + cx.stop_propagation(); + }) + }); }) .detach(); } diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 448f5183b2..8a745af785 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -1,13 +1,18 @@ use super::*; // use crate::undo::tests::{build_create_operation, build_rename_operation}; use collections::HashSet; -use editor::MultiBufferOffset; +use editor::{Editor, MultiBufferOffset}; +use git::{ + Oid, + repository::{InitialGraphCommitData, LogSource, RepoPath}, +}; use gpui::{Empty, Entity, TestAppContext, VisualTestContext}; use menu::Cancel; use pretty_assertions::assert_eq; use project::{FakeFs, ProjectPath}; use serde_json::json; use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; +use smallvec::smallvec; use std::path::{Path, PathBuf}; use util::{path, paths::PathStyle, rel_path::rel_path}; use workspace::{ @@ -167,6 +172,254 @@ async fn test_opening_file(cx: &mut gpui::TestAppContext) { ensure_single_file_is_opened(&workspace, "test/second.rs", cx); } +#[gpui::test] +async fn test_file_history_action_uses_focused_project_panel_selection( + cx: &mut gpui::TestAppContext, +) { + init_test_with_git_ui(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + Path::new("/project"), + json!({ + ".git": {}, + "tracked1.txt": "tracked 1", + "tracked2.txt": "tracked 2", + }), + ) + .await; + + let commits = vec![Arc::new(InitialGraphCommitData { + sha: Oid::from_bytes(&[1; 20]).unwrap(), + parents: smallvec![], + ref_names: vec!["HEAD".into(), "refs/heads/main".into()], + })]; + fs.set_graph_commits(Path::new("/project/.git"), commits); + + let project = Project::test(fs.clone(), [Path::new("/project")], cx).await; + cx.run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project + .active_repository(cx) + .expect("should have active repository") + }); + let project_panel_repo_path = RepoPath::new(&"tracked1.txt").unwrap(); + let editor_repo_path = RepoPath::new(&"tracked2.txt").unwrap(); + let project_panel_path = repository + .read_with(cx, |repository, cx| { + repository.repo_path_to_project_path(&project_panel_repo_path, cx) + }) + .expect("project panel path should resolve"); + let editor_path = repository + .read_with(cx, |repository, cx| { + repository.repo_path_to_project_path(&editor_repo_path, cx) + }) + .expect("editor path should resolve"); + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(&*cx, |multi_workspace, _| { + multi_workspace.workspace().clone() + }); + let project_panel = multi_workspace.update_in(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace(); + workspace.update(cx, |workspace, cx| { + let project_panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(project_panel.clone(), window, cx); + project_panel + }) + }); + cx.run_until_parked(); + + let editor_buffer = project + .update(cx, |project, cx| { + project.open_buffer(editor_path.clone(), cx) + }) + .await + .expect("editor buffer should open"); + multi_workspace.update_in(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace(); + let multibuffer = cx.new(|cx| { + let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite); + multibuffer.set_excerpts_for_buffer( + editor_buffer.clone(), + [Default::default()..editor_buffer.read(cx).max_point()], + 0, + cx, + ); + multibuffer + }); + let editor = + cx.new(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + }); + editor.update(cx, |editor, cx| { + window.focus(&editor.focus_handle(cx), cx); + }); + }); + cx.run_until_parked(); + + multi_workspace.update_in(cx, |_multi_workspace, window, cx| { + project_panel.update(cx, |panel, cx| { + panel.select_path_for_test(project_panel_path.clone(), cx); + }); + project_panel.update(cx, |panel, cx| { + panel.focus_handle(cx).focus(window, cx); + }); + }); + cx.run_until_parked(); + + cx.update(|window, cx| { + window.dispatch_action(Box::new(git::FileHistory), cx); + }); + cx.run_until_parked(); + + workspace.read_with(&*cx, |workspace, cx| { + let graphs = workspace + .items_of_type::(cx) + .collect::>(); + assert_eq!(graphs.len(), 1); + assert_eq!( + graphs[0].read(cx).log_source_for_test(), + &LogSource::Path(project_panel_repo_path) + ); + }); +} + +#[gpui::test] +async fn test_file_history_action_does_not_fall_back_to_editor_when_focused_project_panel_selection_has_no_git_repo( + cx: &mut gpui::TestAppContext, +) { + init_test_with_git_ui(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + Path::new("/git-project"), + json!({ + ".git": {}, + "tracked.txt": "tracked", + }), + ) + .await; + fs.insert_tree( + Path::new("/plain-project"), + json!({ + "plain.txt": "plain", + }), + ) + .await; + + fs.set_graph_commits( + Path::new("/git-project/.git"), + vec![Arc::new(InitialGraphCommitData { + sha: Oid::from_bytes(&[1; 20]).unwrap(), + parents: smallvec![], + ref_names: vec!["HEAD".into(), "refs/heads/main".into()], + })], + ); + + let project = Project::test( + fs.clone(), + [Path::new("/git-project"), Path::new("/plain-project")], + cx, + ) + .await; + cx.run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project + .active_repository(cx) + .expect("should have active repository") + }); + let editor_repo_path = RepoPath::new(&"tracked.txt").unwrap(); + let editor_path = repository + .read_with(cx, |repository, cx| { + repository.repo_path_to_project_path(&editor_repo_path, cx) + }) + .expect("editor path should resolve"); + let plain_worktree_id = project.read_with(cx, |project, cx| { + project + .worktree_for_root_name("plain-project", cx) + .expect("plain worktree should exist") + .read(cx) + .id() + }); + let plain_project_path = ProjectPath { + worktree_id: plain_worktree_id, + path: rel_path("plain.txt").into(), + }; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(&*cx, |multi_workspace, _| { + multi_workspace.workspace().clone() + }); + let project_panel = multi_workspace.update_in(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace(); + workspace.update(cx, |workspace, cx| { + let project_panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(project_panel.clone(), window, cx); + project_panel + }) + }); + cx.run_until_parked(); + + let editor_buffer = project + .update(cx, |project, cx| { + project.open_buffer(editor_path.clone(), cx) + }) + .await + .expect("editor buffer should open"); + multi_workspace.update_in(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace(); + let multibuffer = cx.new(|cx| { + let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite); + multibuffer.set_excerpts_for_buffer( + editor_buffer.clone(), + [Default::default()..editor_buffer.read(cx).max_point()], + 0, + cx, + ); + multibuffer + }); + let editor = + cx.new(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + }); + editor.update(cx, |editor, cx| { + window.focus(&editor.focus_handle(cx), cx); + }); + }); + cx.run_until_parked(); + + multi_workspace.update_in(cx, |_multi_workspace, window, cx| { + project_panel.update(cx, |panel, cx| { + panel.select_path_for_test(plain_project_path.clone(), cx); + }); + project_panel.update(cx, |panel, cx| { + panel.focus_handle(cx).focus(window, cx); + }); + }); + cx.run_until_parked(); + + cx.update(|window, cx| { + window.dispatch_action(Box::new(git::FileHistory), cx); + }); + cx.run_until_parked(); + + workspace.read_with(&*cx, |workspace, cx| { + assert_eq!( + workspace + .items_of_type::(cx) + .count(), + 0 + ); + }); +} + #[gpui::test] async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -10412,6 +10665,27 @@ fn init_test_with_editor(cx: &mut TestAppContext) { }); } +fn init_test_with_git_ui(cx: &mut TestAppContext) { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme_settings::init(theme::LoadThemes::JustBase, cx); + editor::init(cx); + git_ui::init(cx); + crate::init(cx); + workspace::init(app_state, cx); + + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings + .project_panel + .get_or_insert_default() + .auto_fold_dirs = Some(false); + settings.project.worktree.file_scan_exclusions = Some(Vec::new()) + }); + }); + }); +} + fn set_auto_open_settings( cx: &mut TestAppContext, auto_open_settings: ProjectPanelAutoOpenSettings, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 14f5140926..b0cbcb4d08 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -113,7 +113,6 @@ file_finder.workspace = true fs.workspace = true futures.workspace = true git.workspace = true -git_graph.workspace = true git_hosting_providers.workspace = true git_ui.workspace = true go_to_line.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8476392b7a..e9fa128e49 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -767,7 +767,6 @@ fn main() { notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); git_ui::init(cx); - git_graph::init(cx); feedback::init(cx); markdown_preview::init(cx); csv_preview::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f9c04b7c91..7bc26f4679 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5478,7 +5478,6 @@ mod tests { ); language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); web_search::init(cx); - git_graph::init(cx); web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); project::AgentRegistryStore::init_global(