Replace file history actions with git graph UI

Anthony Eid created

Change summary

Cargo.lock                                |   2 
crates/git/src/repository.rs              |   5 
crates/git_graph/Cargo.toml               |   2 
crates/git_graph/src/git_graph.rs         | 477 +++++++++++++++++++-----
crates/git_ui/src/git_panel.rs            |  44 -
crates/project_panel/src/project_panel.rs | 107 +---
6 files changed, 428 insertions(+), 209 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7198,8 +7198,10 @@ dependencies = [
  "git_ui",
  "gpui",
  "language",
+ "language_model",
  "menu",
  "project",
+ "project_panel",
  "rand 0.9.2",
  "remote_connection",
  "search",

crates/git/src/repository.rs 🔗

@@ -2732,7 +2732,6 @@ impl GitRepository for RealGitRepository {
         async move {
             let git = git_binary?;
 
-            // todo!: should we include no optional locks here?
             let mut git_log_command = vec![
                 "log",
                 GRAPH_COMMIT_FORMAT,
@@ -2826,6 +2825,10 @@ impl GitRepository for RealGitRepository {
             args.push("--grep");
             args.push(search_args.query.as_str());
 
+            if let LogSource::File(file_path) = &log_source {
+                args.extend(["--", file_path.as_unix_str()]);
+            }
+
             let mut command = git.build_command(&args);
             command.stdout(Stdio::piped());
             command.stderr(Stdio::null());

crates/git_graph/Cargo.toml 🔗

@@ -30,6 +30,7 @@ gpui.workspace = true
 language.workspace = true
 menu.workspace = true
 project.workspace = true
+project_panel.workspace = true
 search.workspace = true
 settings.workspace = true
 smallvec.workspace = true
@@ -45,6 +46,7 @@ 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"] }

crates/git_graph/src/git_graph.rs 🔗

@@ -26,6 +26,7 @@ use project::{
         RepositoryEvent, RepositoryId,
     },
 };
+use project_panel::ProjectPanel;
 use search::{
     SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch,
     ToggleCaseSensitive, buffer_search,
@@ -734,15 +735,32 @@ pub fn init(cx: &mut App) {
     workspace::register_serializable_item::<GitGraph>(cx);
 
     cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
-        workspace.register_action_renderer(|div, workspace, _, cx| {
-            let active_item_file = workspace
-                .active_item(cx)
-                .and_then(|item| item.downcast::<Editor>())
-                .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
-                .and_then(|buffer| buffer.read(cx).file())
-                .cloned();
-
-            div.when(
+        workspace.register_action_renderer(|div, workspace, window, cx| {
+            div.when_some(
+                resolve_file_history_target(workspace, window, cx),
+                |div, (repo_id, log_source)| {
+                    let git_store = workspace.project().read(cx).git_store().clone();
+                    let workspace = workspace.weak_handle();
+
+                    div.on_action(move |_: &git::FileHistory, window, cx| {
+                        let git_store = git_store.clone();
+                        workspace
+                            .update(cx, |workspace, cx| {
+                                open_or_reuse_graph(
+                                    workspace,
+                                    repo_id,
+                                    git_store,
+                                    log_source.clone(),
+                                    None,
+                                    window,
+                                    cx,
+                                );
+                            })
+                            .ok();
+                    })
+                },
+            )
+            .when(
                 workspace.project().read(cx).active_repository(cx).is_some(),
                 |div| {
                     let workspace = workspace.weak_handle();
@@ -759,31 +777,14 @@ pub fn init(cx: &mut App) {
                                     };
                                     let selected_repo_id = repo.read(cx).id;
 
-                                    let existing = workspace
-                                        .items_of_type::<GitGraph>(cx)
-                                        .find(|graph| graph.read(cx).repo_id == selected_repo_id);
-                                    if let Some(existing) = existing {
-                                        workspace.activate_item(&existing, true, true, window, cx);
-                                        return;
-                                    }
-
                                     let git_store =
                                         workspace.project().read(cx).git_store().clone();
-                                    let workspace_handle = workspace.weak_handle();
-                                    let git_graph = cx.new(|cx| {
-                                        GitGraph::new(
-                                            selected_repo_id,
-                                            git_store,
-                                            workspace_handle,
-                                            None,
-                                            window,
-                                            cx,
-                                        )
-                                    });
-                                    workspace.add_item_to_active_pane(
-                                        Box::new(git_graph),
+                                    open_or_reuse_graph(
+                                        workspace,
+                                        selected_repo_id,
+                                        git_store,
+                                        LogSource::All,
                                         None,
-                                        true,
                                         window,
                                         cx,
                                     );
@@ -803,36 +804,14 @@ pub fn init(cx: &mut App) {
                                     };
                                     let selected_repo_id = repo.read(cx).id;
 
-                                    let existing = workspace
-                                        .items_of_type::<GitGraph>(cx)
-                                        .find(|graph| graph.read(cx).repo_id == selected_repo_id);
-                                    if let Some(existing) = existing {
-                                        existing.update(cx, |graph, cx| {
-                                            graph.select_commit_by_sha(sha.as_str(), cx);
-                                        });
-                                        workspace.activate_item(&existing, true, true, window, cx);
-                                        return;
-                                    }
-
                                     let git_store =
                                         workspace.project().read(cx).git_store().clone();
-                                    let workspace_handle = workspace.weak_handle();
-                                    let git_graph = cx.new(|cx| {
-                                        let mut graph = GitGraph::new(
-                                            selected_repo_id,
-                                            git_store,
-                                            workspace_handle,
-                                            None,
-                                            window,
-                                            cx,
-                                        );
-                                        graph.select_commit_by_sha(sha.as_str(), cx);
-                                        graph
-                                    });
-                                    workspace.add_item_to_active_pane(
-                                        Box::new(git_graph),
-                                        None,
-                                        true,
+                                    open_or_reuse_graph(
+                                        workspace,
+                                        selected_repo_id,
+                                        git_store,
+                                        LogSource::All,
+                                        Some(sha),
                                         window,
                                         cx,
                                     );
@@ -842,58 +821,91 @@ pub fn init(cx: &mut App) {
                     )
                 },
             )
-            .when_some(active_item_file, move |this, active_file| {
-                this.on_action({
-                    let workspace = workspace.weak_handle();
+        });
+    })
+    .detach();
+}
 
-                    move |_: &git::FileHistory, window, cx| {
-                        workspace
-                            .update(cx, |workspace, cx| {
-                                let git_store = workspace.project().read(cx).git_store().clone();
-                                let workspace_handle = workspace.weak_handle();
-                                let file_path = active_file.path();
-                                let file_worktree_id = active_file.worktree_id(cx);
-
-                                let project_path = ProjectPath {
-                                    worktree_id: file_worktree_id,
-                                    path: file_path.clone(),
-                                };
+fn resolve_file_history_target(
+    workspace: &Workspace,
+    window: &Window,
+    cx: &App,
+) -> Option<(RepositoryId, LogSource)> {
+    if let Some(panel) = workspace.panel::<ProjectPanel>(cx)
+        && panel.read(cx).focus_handle(cx).contains_focused(window, cx)
+        && let Some(project_path) = panel.read(cx).selected_file_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)?;
+        return Some((repo.read(cx).id, LogSource::File(repo_path)));
+    }
 
-                                let Some((repo, repo_path)) = git_store
-                                    .read(cx)
-                                    .repository_and_path_for_project_path(&project_path, cx)
-                                else {
-                                    return;
-                                };
+    if let Some(panel) = workspace.panel::<git_ui::git_panel::GitPanel>(cx)
+        && panel.read(cx).focus_handle(cx).contains_focused(window, cx)
+        && let Some((repository, repo_path)) = panel.read(cx).selected_file_history_target()
+    {
+        return Some((repository.read(cx).id, LogSource::File(repo_path)));
+    }
 
-                                let repo_id = repo.read(cx).id;
-                                let log_source = LogSource::File(repo_path);
+    let editor = workspace.active_item_as::<Editor>(cx)?;
 
-                                let git_graph = cx.new(|cx| {
-                                    GitGraph::new(
-                                        repo_id,
-                                        git_store,
-                                        workspace_handle,
-                                        Some(log_source),
-                                        window,
-                                        cx,
-                                    )
-                                });
-                                workspace.add_item_to_active_pane(
-                                    Box::new(git_graph),
-                                    None,
-                                    true,
-                                    window,
-                                    cx,
-                                );
-                            })
-                            .ok();
-                    }
-                })
-            })
-        });
-    })
-    .detach();
+    let file = editor
+        .read(cx)
+        .file_at(editor.read(cx).selections.newest_anchor().head(), cx)?;
+    let project_path = ProjectPath {
+        worktree_id: file.worktree_id(cx),
+        path: file.path().clone(),
+    };
+
+    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)?;
+    Some((repo.read(cx).id, LogSource::File(repo_path)))
+}
+
+fn open_or_reuse_graph(
+    workspace: &mut Workspace,
+    repo_id: RepositoryId,
+    git_store: Entity<GitStore>,
+    log_source: LogSource,
+    sha: Option<String>,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let existing = workspace.items_of_type::<GitGraph>(cx).find(|graph| {
+        let graph = graph.read(cx);
+        graph.repo_id == repo_id && graph.log_source == log_source
+    });
+
+    if let Some(existing) = existing {
+        if let Some(sha) = sha {
+            existing.update(cx, |graph, cx| {
+                graph.select_commit_by_sha(sha.as_str(), cx);
+            });
+        }
+        workspace.activate_item(&existing, true, true, window, cx);
+        return;
+    }
+
+    let workspace_handle = workspace.weak_handle();
+    let git_graph = cx.new(|cx| {
+        let mut graph = GitGraph::new(
+            repo_id,
+            git_store,
+            workspace_handle,
+            Some(log_source),
+            window,
+            cx,
+        );
+        if let Some(sha) = sha {
+            graph.select_commit_by_sha(sha.as_str(), cx);
+        }
+        graph
+    });
+    workspace.add_item_to_active_pane(Box::new(git_graph), None, true, window, cx);
 }
 
 fn lane_center_x(bounds: Bounds<Pixels>, lane: f32) -> Pixels {
@@ -1628,9 +1640,11 @@ impl GitGraph {
                 .and_then(|data| data.commit_oid_to_index.get(&oid))
                 .copied()
             else {
+                this.pending_select_sha = Some(oid);
                 return;
             };
 
+            this.pending_select_sha = None;
             this.select_entry(index, ScrollStrategy::Center, cx);
         }
 
@@ -2910,11 +2924,22 @@ impl Item for GitGraph {
                 .file_name()
                 .map(|name| name.to_string_lossy().to_string())
         });
+        let file_history_path = match &self.log_source {
+            LogSource::File(path) => Some(path.as_unix_str().to_string()),
+            _ => None,
+        };
 
         Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
             move |_, _| {
                 v_flex()
-                    .child(Label::new("Git Graph"))
+                    .child(Label::new(if file_history_path.is_some() {
+                        "File History"
+                    } else {
+                        "Git Graph"
+                    }))
+                    .when_some(file_history_path.clone(), |this, path| {
+                        this.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
+                    })
                     .when_some(repo_name.clone(), |this, name| {
                         this.child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
                     })
@@ -2924,6 +2949,14 @@ impl Item for GitGraph {
     }
 
     fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
+        if let LogSource::File(path) = &self.log_source {
+            return path
+                .as_ref()
+                .file_name()
+                .map(|name| SharedString::from(name.to_string()))
+                .unwrap_or_else(|| SharedString::from(path.as_unix_str().to_string()));
+        }
+
         self.get_repository(cx)
             .and_then(|repo| {
                 repo.read(cx)
@@ -3117,6 +3150,10 @@ mod tests {
             let settings_store = SettingsStore::test(cx);
             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);
         });
     }
 
@@ -3942,6 +3979,230 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_file_history_action_uses_focused_source_and_reuses_matching_graph(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(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 tracked1_repo_path = RepoPath::new(&"tracked1.txt").unwrap();
+        let tracked2_repo_path = RepoPath::new(&"tracked2.txt").unwrap();
+        let tracked1 = repository
+            .read_with(cx, |repository, cx| {
+                repository.repo_path_to_project_path(&tracked1_repo_path, cx)
+            })
+            .expect("tracked1 should resolve to project path");
+        let tracked2 = repository
+            .read_with(cx, |repository, cx| {
+                repository.repo_path_to_project_path(&tracked2_repo_path, cx)
+            })
+            .expect("tracked2 should resolve to project path");
+
+        let workspace_window = cx.add_window(|window, cx| {
+            workspace::MultiWorkspace::test_new(project.clone(), window, cx)
+        });
+        let workspace = workspace_window
+            .read_with(cx, |multi, _| multi.workspace().clone())
+            .expect("workspace should exist");
+
+        let (weak_workspace, async_window_cx) = workspace_window
+            .update(cx, |multi, window, cx| {
+                (multi.workspace().downgrade(), window.to_async(cx))
+            })
+            .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(
+                weak_workspace,
+                async_window_cx,
+            ))
+            .expect("git panel should load");
+        cx.background_executor.forbid_parking();
+
+        workspace_window
+            .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.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)
+                });
+                workspace.update(cx, |workspace, cx| {
+                    workspace.focus_panel::<ProjectPanel>(window, cx);
+                });
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+        workspace_window
+            .update(cx, |_, window, cx| {
+                window.dispatch_action(Box::new(git::FileHistory), cx);
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+
+        workspace.read_with(cx, |workspace, cx| {
+            let graphs = workspace.items_of_type::<GitGraph>(cx).collect::<Vec<_>>();
+            assert_eq!(graphs.len(), 1);
+            assert_eq!(
+                graphs[0].read(cx).log_source,
+                LogSource::File(tracked1_repo_path.clone())
+            );
+        });
+
+        workspace_window
+            .update(cx, |multi, window, cx| {
+                let workspace = multi.workspace();
+                git_panel.update(cx, |panel, cx| {
+                    panel.select_entry_by_path(tracked1.clone(), window, cx);
+                });
+                workspace.update(cx, |workspace, cx| {
+                    workspace.focus_panel::<git_ui::git_panel::GitPanel>(window, cx);
+                });
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+        workspace_window
+            .update(cx, |_, window, cx| {
+                window.dispatch_action(Box::new(git::FileHistory), cx);
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+
+        workspace.read_with(cx, |workspace, cx| {
+            let graphs = workspace.items_of_type::<GitGraph>(cx).collect::<Vec<_>>();
+            assert_eq!(graphs.len(), 1);
+            assert_eq!(
+                graphs[0].read(cx).log_source,
+                LogSource::File(tracked1_repo_path.clone())
+            );
+        });
+
+        let tracked1_buffer = project
+            .update(cx, |project, cx| project.open_buffer(tracked1.clone(), cx))
+            .await
+            .expect("tracked1 buffer should open");
+        let tracked2_buffer = project
+            .update(cx, |project, cx| project.open_buffer(tracked2.clone(), cx))
+            .await
+            .expect("tracked2 buffer should open");
+        workspace_window
+            .update(cx, |multi, window, cx| {
+                let workspace = multi.workspace();
+                let multibuffer = cx.new(|cx| {
+                    let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite);
+                    multibuffer.set_excerpts_for_buffer(
+                        tracked1_buffer.clone(),
+                        [Default::default()..tracked1_buffer.read(cx).max_point()],
+                        0,
+                        cx,
+                    );
+                    multibuffer.set_excerpts_for_buffer(
+                        tracked2_buffer.clone(),
+                        [Default::default()..tracked2_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| {
+                    let snapshot = editor.buffer().read(cx).snapshot(cx);
+                    let second_excerpt_point = snapshot
+                        .range_for_buffer(tracked2_buffer.read(cx).remote_id())
+                        .expect("tracked2 excerpt should exist")
+                        .start;
+                    let anchor = snapshot.anchor_before(second_excerpt_point);
+                    editor.change_selections(
+                        editor::SelectionEffects::no_scroll(),
+                        window,
+                        cx,
+                        |selections| {
+                            selections.select_anchor_ranges([anchor..anchor]);
+                        },
+                    );
+                    window.focus(&editor.focus_handle(cx), cx);
+                });
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+
+        workspace_window
+            .update(cx, |_, window, cx| {
+                window.dispatch_action(Box::new(git::FileHistory), cx);
+            })
+            .expect("workspace window should be available");
+        cx.run_until_parked();
+
+        workspace.read_with(cx, |workspace, cx| {
+            let graphs = workspace.items_of_type::<GitGraph>(cx).collect::<Vec<_>>();
+            assert_eq!(graphs.len(), 2);
+            let latest = graphs
+                .into_iter()
+                .max_by_key(|graph| graph.entity_id())
+                .expect("expected a git graph");
+            assert_eq!(
+                latest.read(cx).log_source,
+                LogSource::File(tracked2_repo_path)
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_graph_data_reloaded_after_stash_change(cx: &mut TestAppContext) {
         init_test(cx);

crates/git_ui/src/git_panel.rs 🔗

@@ -7,8 +7,7 @@ use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
 use crate::remote_output::{self, RemoteAction, SuccessMessage};
 use crate::{branch_picker, picker_prompt, render_remote_button};
 use crate::{
-    file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon,
-    repository_selector::RepositorySelector,
+    git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
 };
 use agent_settings::AgentSettings;
 use anyhow::Context as _;
@@ -1300,26 +1299,6 @@ impl GitPanel {
         });
     }
 
-    fn file_history(&mut self, _: &git::FileHistory, window: &mut Window, cx: &mut Context<Self>) {
-        maybe!({
-            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
-            let active_repo = self.active_repository.as_ref()?;
-            let repo_path = entry.repo_path.clone();
-            let git_store = self.project.read(cx).git_store();
-
-            FileHistoryView::open(
-                repo_path,
-                git_store.downgrade(),
-                active_repo.downgrade(),
-                self.workspace.clone(),
-                window,
-                cx,
-            );
-
-            Some(())
-        });
-    }
-
     fn open_file(
         &mut self,
         _: &menu::SecondaryConfirm,
@@ -4983,8 +4962,11 @@ impl GitPanel {
                 .separator()
                 .action("Open Diff", menu::Confirm.boxed_clone())
                 .action("Open File", menu::SecondaryConfirm.boxed_clone())
-                .separator()
-                .action_disabled_when(is_created, "View File History", Box::new(git::FileHistory))
+                .when(!is_created, |context_menu| {
+                    context_menu
+                        .separator()
+                        .action("View File History", Box::new(git::FileHistory))
+                })
         });
         self.selected_entry = Some(ix);
         self.set_context_menu(context_menu, position, window, cx);
@@ -5617,6 +5599,17 @@ impl GitPanel {
     }
 }
 
+impl GitPanel {
+    pub fn selected_file_history_target(&self) -> Option<(Entity<Repository>, RepoPath)> {
+        let entry = self.get_selected_entry()?.status_entry()?;
+        let repository = self.active_repository.clone()?;
+        if entry.status.is_created() {
+            return None;
+        }
+        Some((repository, entry.repo_path.clone()))
+    }
+}
+
 #[cfg(any(test, feature = "test-support"))]
 impl GitPanel {
     pub fn new_test(
@@ -5685,9 +5678,6 @@ impl Render for GitPanel {
             .on_action(cx.listener(Self::close_panel))
             .on_action(cx.listener(Self::open_diff))
             .on_action(cx.listener(Self::open_file))
-            // TODO!: We should remove this listener, so that git graph
-            // implementation of file history is used.
-            .on_action(cx.listener(Self::file_history))
             .on_action(cx.listener(Self::focus_changes_list))
             .on_action(cx.listener(Self::focus_editor))
             .on_action(cx.listener(Self::expand_commit_editor))

crates/project_panel/src/project_panel.rs 🔗

@@ -523,74 +523,6 @@ pub fn init(cx: &mut App) {
                 panel.update(cx, |panel, cx| panel.delete(action, window, cx));
             }
         });
-
-        // TODO!: We should remove this `register_action` call, so that git
-        // graph implementation of file history is used.
-        workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
-            // First try to get from project panel if it's focused
-            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
-                let maybe_project_path = panel.read(cx).selection.and_then(|selection| {
-                    let project = workspace.project().read(cx);
-                    let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
-                    let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
-                    if entry.is_file() {
-                        Some(ProjectPath {
-                            worktree_id: selection.worktree_id,
-                            path: entry.path.clone(),
-                        })
-                    } else {
-                        None
-                    }
-                });
-
-                if let Some(project_path) = maybe_project_path {
-                    let project = workspace.project();
-                    let git_store = project.read(cx).git_store();
-                    if let Some((repo, repo_path)) = git_store
-                        .read(cx)
-                        .repository_and_path_for_project_path(&project_path, cx)
-                    {
-                        git_ui::file_history_view::FileHistoryView::open(
-                            repo_path,
-                            git_store.downgrade(),
-                            repo.downgrade(),
-                            workspace.weak_handle(),
-                            window,
-                            cx,
-                        );
-                        return;
-                    }
-                }
-            }
-
-            // Fallback: try to get from active editor
-            if let Some(active_item) = workspace.active_item(cx)
-                && let Some(editor) = active_item.downcast::<Editor>()
-                && let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
-                && let Some(file) = buffer.read(cx).file()
-            {
-                let worktree_id = file.worktree_id(cx);
-                let project_path = ProjectPath {
-                    worktree_id,
-                    path: file.path().clone(),
-                };
-                let project = workspace.project();
-                let git_store = project.read(cx).git_store();
-                if let Some((repo, repo_path)) = git_store
-                    .read(cx)
-                    .repository_and_path_for_project_path(&project_path, cx)
-                {
-                    git_ui::file_history_view::FileHistoryView::open(
-                        repo_path,
-                        git_store.downgrade(),
-                        repo.downgrade(),
-                        workspace.weak_handle(),
-                        window,
-                        cx,
-                    );
-                }
-            }
-        });
     })
     .detach();
 }
@@ -1115,16 +1047,18 @@ impl ProjectPanel {
                     || (settings.hide_root && visible_worktrees_count == 1));
             let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
 
-            let has_git_repo = !is_dir && {
+            let has_file_history = !is_dir && {
                 let project_path = project::ProjectPath {
                     worktree_id,
                     path: entry.path.clone(),
                 };
-                project
-                    .git_store()
-                    .read(cx)
+                let git_store = project.git_store().read(cx);
+                git_store
                     .repository_and_path_for_project_path(&project_path, cx)
                     .is_some()
+                    && !git_store
+                        .project_path_git_status(&project_path, cx)
+                        .is_some_and(|status| status.is_created())
             };
 
             let has_pasteable_content = self.has_pasteable_content(cx);
@@ -1192,7 +1126,7 @@ impl ProjectPanel {
                                     Box::new(git::RestoreFile { skip_prompt: false }),
                                 )
                             })
-                            .when(has_git_repo, |menu| {
+                            .when(has_file_history, |menu| {
                                 menu.separator()
                                     .action("View File History", Box::new(git::FileHistory))
                             })
@@ -3777,6 +3711,14 @@ impl ProjectPanel {
         Some((worktree.read(cx), entry))
     }
 
+    pub fn selected_file_project_path(&self, cx: &App) -> Option<ProjectPath> {
+        let (worktree, entry) = self.selected_sub_entry(cx)?;
+        Some(ProjectPath {
+            worktree_id: worktree.read(cx).id(),
+            path: entry.is_file().then(|| entry.path.clone())?,
+        })
+    }
+
     /// Compared to selected_entry, this function resolves to the currently
     /// selected subentry if dir auto-folding is enabled.
     fn selected_sub_entry<'a>(
@@ -7249,6 +7191,25 @@ impl Panel for ProjectPanel {
     }
 }
 
+impl ProjectPanel {
+    pub fn select_path_for_test(&mut self, project_path: ProjectPath, cx: &App) {
+        let Some(worktree) = self
+            .project
+            .read(cx)
+            .worktree_for_id(project_path.worktree_id, cx)
+        else {
+            return;
+        };
+        let Some(entry) = worktree.read(cx).entry_for_path(project_path.path.as_ref()) else {
+            return;
+        };
+        self.selection = Some(SelectedEntry {
+            worktree_id: project_path.worktree_id,
+            entry_id: entry.id,
+        });
+    }
+}
+
 impl Focusable for ProjectPanel {
     fn focus_handle(&self, _cx: &App) -> FocusHandle {
         self.focus_handle.clone()