Remove file history view and make git graph fields persistent

Anthony Eid created

The graph now remembers it's search text, case sensitive toggle,
selected entry, and the log source/order variant.

Change summary

crates/fs/src/fake_git_repo.rs         |  21 
crates/git/src/repository.rs           | 109 ----
crates/git_graph/src/git_graph.rs      | 458 ++++++++++++++++++++
crates/git_ui/src/file_history_view.rs | 613 ----------------------------
crates/git_ui/src/git_ui.rs            |   1 
crates/project/src/git_store.rs        | 113 -----
crates/proto/proto/git.proto           |  23 -
crates/proto/proto/zed.proto           |   3 
crates/proto/src/proto.rs              |   4 
9 files changed, 449 insertions(+), 896 deletions(-)

Detailed changes

crates/fs/src/fake_git_repo.rs 🔗

@@ -142,7 +142,7 @@ impl GitRepository for FakeGitRepository {
         _commit: String,
         _cx: AsyncApp,
     ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
-        unimplemented!()
+        async { Ok(git::repository::CommitDiff { files: Vec::new() }) }.boxed()
     }
 
     fn set_index_text(
@@ -782,25 +782,6 @@ impl GitRepository for FakeGitRepository {
         })
     }
 
-    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
-        self.file_history_paginated(path, 0, None)
-    }
-
-    fn file_history_paginated(
-        &self,
-        path: RepoPath,
-        _skip: usize,
-        _limit: Option<usize>,
-    ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
-        async move {
-            Ok(git::repository::FileHistory {
-                entries: Vec::new(),
-                path,
-            })
-        }
-        .boxed()
-    }
-
     fn stage_paths(
         &self,
         paths: Vec<RepoPath>,

crates/git/src/repository.rs 🔗

@@ -396,22 +396,6 @@ pub struct CommitDetails {
     pub author_name: SharedString,
 }
 
-#[derive(Clone, Debug, Hash, PartialEq, Eq)]
-pub struct FileHistoryEntry {
-    pub sha: SharedString,
-    pub subject: SharedString,
-    pub message: SharedString,
-    pub commit_timestamp: i64,
-    pub author_name: SharedString,
-    pub author_email: SharedString,
-}
-
-#[derive(Debug, Clone)]
-pub struct FileHistory {
-    pub entries: Vec<FileHistoryEntry>,
-    pub path: RepoPath,
-}
-
 #[derive(Debug)]
 pub struct CommitDiff {
     pub files: Vec<CommitFile>,
@@ -751,13 +735,6 @@ pub trait GitRepository: Send + Sync {
         content: Rope,
         line_ending: LineEnding,
     ) -> BoxFuture<'_, Result<crate::blame::Blame>>;
-    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>>;
-    fn file_history_paginated(
-        &self,
-        path: RepoPath,
-        skip: usize,
-        limit: Option<usize>,
-    ) -> BoxFuture<'_, Result<FileHistory>>;
 
     /// Returns the absolute path to the repository. For worktrees, this will be the path to the
     /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
@@ -1846,92 +1823,6 @@ impl GitRepository for RealGitRepository {
             .boxed()
     }
 
-    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>> {
-        self.file_history_paginated(path, 0, None)
-    }
-
-    fn file_history_paginated(
-        &self,
-        path: RepoPath,
-        skip: usize,
-        limit: Option<usize>,
-    ) -> BoxFuture<'_, Result<FileHistory>> {
-        let git_binary = self.git_binary();
-        self.executor
-            .spawn(async move {
-                let git = git_binary?;
-                // Use a unique delimiter with a hardcoded UUID to separate commits
-                // This essentially eliminates any chance of encountering the delimiter in actual commit data
-                let commit_delimiter =
-                    concat!("<<COMMIT_END-", "3f8a9c2e-7d4b-4e1a-9f6c-8b5d2a1e4c3f>>",);
-
-                let format_string = format!(
-                    "--pretty=format:%H%x00%s%x00%B%x00%at%x00%an%x00%ae{}",
-                    commit_delimiter
-                );
-
-                let mut args = vec!["log", "--follow", &format_string];
-
-                let skip_str;
-                let limit_str;
-                if skip > 0 {
-                    skip_str = skip.to_string();
-                    args.push("--skip");
-                    args.push(&skip_str);
-                }
-                if let Some(n) = limit {
-                    limit_str = n.to_string();
-                    args.push("-n");
-                    args.push(&limit_str);
-                }
-
-                args.push("--");
-
-                let output = git
-                    .build_command(&args)
-                    .arg(path.as_unix_str())
-                    .output()
-                    .await?;
-
-                if !output.status.success() {
-                    let stderr = String::from_utf8_lossy(&output.stderr);
-                    bail!("git log failed: {stderr}");
-                }
-
-                let stdout = std::str::from_utf8(&output.stdout)?;
-                let mut entries = Vec::new();
-
-                for commit_block in stdout.split(commit_delimiter) {
-                    let commit_block = commit_block.trim();
-                    if commit_block.is_empty() {
-                        continue;
-                    }
-
-                    let fields: Vec<&str> = commit_block.split('\0').collect();
-                    if fields.len() >= 6 {
-                        let sha = fields[0].trim().to_string().into();
-                        let subject = fields[1].trim().to_string().into();
-                        let message = fields[2].trim().to_string().into();
-                        let commit_timestamp = fields[3].trim().parse().unwrap_or(0);
-                        let author_name = fields[4].trim().to_string().into();
-                        let author_email = fields[5].trim().to_string().into();
-
-                        entries.push(FileHistoryEntry {
-                            sha,
-                            subject,
-                            message,
-                            commit_timestamp,
-                            author_name,
-                            author_email,
-                        });
-                    }
-                }
-
-                Ok(FileHistory { entries, path })
-            })
-            .boxed()
-    }
-
     fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
         let git_binary = self.git_binary();
         self.executor

crates/git_graph/src/git_graph.rs 🔗

@@ -997,6 +997,7 @@ impl GitGraph {
         self.search_state.matches.clear();
         self.search_state.selected_index = None;
         self.search_state.state.next_state();
+        cx.emit(ItemEvent::Edit);
         cx.notify();
     }
 
@@ -1250,17 +1251,18 @@ impl GitGraph {
                 }
             }
             RepositoryEvent::HeadChanged | RepositoryEvent::BranchListChanged => {
-                self.pending_select_sha = None;
                 // Only invalidate if we scanned atleast once,
                 // meaning we are not inside the initial repo loading state
                 // NOTE: this fixes an loading performance regression
                 if repository.read(cx).scan_id > 1 {
+                    self.pending_select_sha = None;
                     self.invalidate_state(cx);
                 }
             }
             RepositoryEvent::StashEntriesChanged if self.log_source == LogSource::All => {
-                self.pending_select_sha = None;
-                if repository.read(cx).scan_id > 1 {
+                // Stash entries initial's scan id is 2, so we don't want to invalidate the graph before that
+                if repository.read(cx).scan_id > 2 {
+                    self.pending_select_sha = None;
                     self.invalidate_state(cx);
                 }
             }
@@ -1435,6 +1437,7 @@ impl GitGraph {
         self.selected_entry_idx = None;
         self.selected_commit_diff = None;
         self.selected_commit_diff_stats = None;
+        cx.emit(ItemEvent::Edit);
         cx.notify();
     }
 
@@ -1544,6 +1547,7 @@ impl GitGraph {
         });
 
         self.search_state.state = QueryState::Confirmed((query, search_task));
+        cx.emit(ItemEvent::Edit);
     }
 
     fn confirm_search(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
@@ -1595,6 +1599,7 @@ impl GitGraph {
             }
         }));
 
+        cx.emit(ItemEvent::Edit);
         cx.notify();
     }
 
@@ -2926,6 +2931,7 @@ impl Render for GitGraph {
             .on_action(cx.listener(|this, _: &ToggleCaseSensitive, _window, cx| {
                 this.search_state.case_sensitive = !this.search_state.case_sensitive;
                 this.search_state.state.next_state();
+                cx.emit(ItemEvent::Edit);
                 cx.notify();
             }))
             .child(
@@ -3053,10 +3059,28 @@ impl workspace::SerializableItem for GitGraph {
         cx: &mut App,
     ) -> Task<gpui::Result<Entity<Self>>> {
         let db = persistence::GitGraphsDb::global(cx);
-        let Some(repo_work_path) = db.get_git_graph(item_id, workspace_id).ok().flatten() else {
+        let Some((
+            repo_work_path,
+            log_source_type,
+            log_source_value,
+            log_order,
+            selected_sha,
+            search_query,
+            search_case_sensitive,
+        )) = db.get_git_graph(item_id, workspace_id).ok().flatten()
+        else {
             return Task::ready(Err(anyhow::anyhow!("No git graph to deserialize")));
         };
 
+        let state = persistence::SerializedGitGraphState {
+            log_source_type,
+            log_source_value,
+            log_order,
+            selected_sha,
+            search_query,
+            search_case_sensitive,
+        };
+
         let window_handle = window.window_handle();
         let project = project.read(cx);
         let git_store = project.git_store().clone();
@@ -3081,7 +3105,37 @@ impl workspace::SerializableItem for GitGraph {
                     return Err(anyhow::anyhow!("Repository not found for path: {:?}", path));
                 };
 
-                Ok(cx.new(|cx| GitGraph::new(repo_id, git_store, workspace, None, window, cx)))
+                let log_source = persistence::deserialize_log_source(&state);
+                let log_order = persistence::deserialize_log_order(&state);
+
+                let git_graph = cx.new(|cx| {
+                    let mut graph =
+                        GitGraph::new(repo_id, git_store, workspace, Some(log_source), window, cx);
+                    graph.log_order = log_order;
+
+                    if let Some(sha) = &state.selected_sha {
+                        graph.select_commit_by_sha(sha.as_str(), cx);
+                    }
+
+                    graph
+                });
+
+                git_graph.update(cx, |graph, cx| {
+                    graph.search_state.case_sensitive =
+                        state.search_case_sensitive.unwrap_or(false);
+
+                    if let Some(query) = &state.search_query
+                        && !query.is_empty()
+                    {
+                        graph
+                            .search_state
+                            .editor
+                            .update(cx, |editor, cx| editor.set_text(query.as_str(), window, cx));
+                        graph.search(query.clone().into(), cx);
+                    }
+                });
+
+                Ok(git_graph)
             })?
         })
     }
@@ -3103,26 +3157,60 @@ impl workspace::SerializableItem for GitGraph {
             .to_string_lossy()
             .to_string();
 
+        let selected_sha = self
+            .selected_entry_idx
+            .and_then(|idx| self.graph_data.commits.get(idx))
+            .map(|commit| commit.data.sha.to_string());
+
+        let search_query = self.search_state.editor.read(cx).text(cx);
+        let search_query = if search_query.is_empty() {
+            None
+        } else {
+            Some(search_query)
+        };
+
+        let log_source_type = Some(persistence::serialize_log_source_type(&self.log_source));
+        let log_source_value = persistence::serialize_log_source_value(&self.log_source);
+        let log_order = Some(persistence::serialize_log_order(&self.log_order));
+        let search_case_sensitive = Some(self.search_state.case_sensitive);
+
         let db = persistence::GitGraphsDb::global(cx);
         Some(cx.background_spawn(async move {
-            db.save_git_graph(item_id, workspace_id, repo_working_path)
-                .await
+            db.save_git_graph(
+                item_id,
+                workspace_id,
+                repo_working_path,
+                log_source_type,
+                log_source_value,
+                log_order,
+                selected_sha,
+                search_query,
+                search_case_sensitive,
+            )
+            .await
         }))
     }
 
     fn should_serialize(&self, event: &Self::Event) -> bool {
-        event == &ItemEvent::UpdateTab
+        match event {
+            ItemEvent::UpdateTab | ItemEvent::Edit => true,
+            _ => false,
+        }
     }
 }
 
 mod persistence {
-    use std::path::PathBuf;
+    use std::{path::PathBuf, str::FromStr};
 
     use db::{
         query,
         sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
         sqlez_macros::sql,
     };
+    use git::{
+        Oid,
+        repository::{LogOrder, LogSource, RepoPath},
+    };
     use workspace::WorkspaceDb;
 
     pub struct GitGraphsDb(ThreadSafeConnection);
@@ -3145,20 +3233,119 @@ mod persistence {
             sql!(
                 ALTER TABLE git_graphs ADD COLUMN repo_working_path TEXT;
             ),
+            sql!(
+                ALTER TABLE git_graphs ADD COLUMN log_source_type TEXT;
+                ALTER TABLE git_graphs ADD COLUMN log_source_value TEXT;
+                ALTER TABLE git_graphs ADD COLUMN log_order TEXT;
+                ALTER TABLE git_graphs ADD COLUMN selected_sha TEXT;
+                ALTER TABLE git_graphs ADD COLUMN search_query TEXT;
+                ALTER TABLE git_graphs ADD COLUMN search_case_sensitive INTEGER;
+            ),
         ];
     }
 
     db::static_connection!(GitGraphsDb, [WorkspaceDb]);
 
+    pub const LOG_SOURCE_ALL: i32 = 0;
+    pub const LOG_SOURCE_BRANCH: i32 = 1;
+    pub const LOG_SOURCE_SHA: i32 = 2;
+    pub const LOG_SOURCE_FILE: i32 = 3;
+
+    pub const LOG_ORDER_DATE: i32 = 0;
+    pub const LOG_ORDER_TOPO: i32 = 1;
+    pub const LOG_ORDER_AUTHOR_DATE: i32 = 2;
+    pub const LOG_ORDER_REVERSE: i32 = 3;
+
+    pub fn serialize_log_source_type(log_source: &LogSource) -> i32 {
+        match log_source {
+            LogSource::All => LOG_SOURCE_ALL,
+            LogSource::Branch(_) => LOG_SOURCE_BRANCH,
+            LogSource::Sha(_) => LOG_SOURCE_SHA,
+            LogSource::File(_) => LOG_SOURCE_FILE,
+        }
+    }
+
+    pub fn serialize_log_source_value(log_source: &LogSource) -> Option<String> {
+        match log_source {
+            LogSource::All => None,
+            LogSource::Branch(branch) => Some(branch.to_string()),
+            LogSource::Sha(oid) => Some(oid.to_string()),
+            LogSource::File(path) => Some(path.as_unix_str().to_string()),
+        }
+    }
+
+    pub fn serialize_log_order(log_order: &LogOrder) -> i32 {
+        match log_order {
+            LogOrder::DateOrder => LOG_ORDER_DATE,
+            LogOrder::TopoOrder => LOG_ORDER_TOPO,
+            LogOrder::AuthorDateOrder => LOG_ORDER_AUTHOR_DATE,
+            LogOrder::ReverseChronological => LOG_ORDER_REVERSE,
+        }
+    }
+
+    pub fn deserialize_log_source(state: &SerializedGitGraphState) -> LogSource {
+        match state.log_source_type {
+            Some(LOG_SOURCE_ALL) => LogSource::All,
+            Some(LOG_SOURCE_BRANCH) => state
+                .log_source_value
+                .as_ref()
+                .map(|v| LogSource::Branch(v.clone().into()))
+                .unwrap_or_default(),
+            Some(LOG_SOURCE_SHA) => state
+                .log_source_value
+                .as_ref()
+                .and_then(|v| Oid::from_str(v).ok())
+                .map(LogSource::Sha)
+                .unwrap_or_default(),
+            Some(LOG_SOURCE_FILE) => state
+                .log_source_value
+                .as_ref()
+                .and_then(|v| RepoPath::new(v).ok())
+                .map(LogSource::File)
+                .unwrap_or_default(),
+            None | Some(_) => LogSource::default(),
+        }
+    }
+
+    pub fn deserialize_log_order(state: &SerializedGitGraphState) -> LogOrder {
+        match state.log_order {
+            Some(LOG_ORDER_DATE) => LogOrder::DateOrder,
+            Some(LOG_ORDER_TOPO) => LogOrder::TopoOrder,
+            Some(LOG_ORDER_AUTHOR_DATE) => LogOrder::AuthorDateOrder,
+            Some(LOG_ORDER_REVERSE) => LogOrder::ReverseChronological,
+            _ => LogOrder::default(),
+        }
+    }
+
+    #[derive(Debug, Default, Clone)]
+    pub struct SerializedGitGraphState {
+        pub log_source_type: Option<i32>,
+        pub log_source_value: Option<String>,
+        pub log_order: Option<i32>,
+        pub selected_sha: Option<String>,
+        pub search_query: Option<String>,
+        pub search_case_sensitive: Option<bool>,
+    }
+
     impl GitGraphsDb {
         query! {
             pub async fn save_git_graph(
                 item_id: workspace::ItemId,
                 workspace_id: workspace::WorkspaceId,
-                repo_working_path: String
+                repo_working_path: String,
+                log_source_type: Option<i32>,
+                log_source_value: Option<String>,
+                log_order: Option<i32>,
+                selected_sha: Option<String>,
+                search_query: Option<String>,
+                search_case_sensitive: Option<bool>
             ) -> Result<()> {
-                INSERT OR REPLACE INTO git_graphs(item_id, workspace_id, repo_working_path)
-                VALUES (?, ?, ?)
+                INSERT OR REPLACE INTO git_graphs(
+                    item_id, workspace_id, repo_working_path,
+                    log_source_type, log_source_value, log_order,
+                    selected_sha, search_query, search_case_sensitive
+                )
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
             }
         }
 
@@ -3166,8 +3353,23 @@ mod persistence {
             pub fn get_git_graph(
                 item_id: workspace::ItemId,
                 workspace_id: workspace::WorkspaceId
-            ) -> Result<Option<PathBuf>> {
-                SELECT repo_working_path
+            ) -> Result<Option<(
+                PathBuf,
+                Option<i32>,
+                Option<String>,
+                Option<i32>,
+                Option<String>,
+                Option<String>,
+                Option<bool>
+            )>> {
+                SELECT
+                    repo_working_path,
+                    log_source_type,
+                    log_source_value,
+                    log_order,
+                    selected_sha,
+                    search_query,
+                    search_case_sensitive
                 FROM git_graphs
                 WHERE item_id = ? AND workspace_id = ?
             }
@@ -4251,6 +4453,234 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_serialized_state_roundtrip(_cx: &mut TestAppContext) {
+        use persistence::SerializedGitGraphState;
+
+        let file_path = RepoPath::new(&"src/main.rs").unwrap();
+        let sha = Oid::from_bytes(&[0xab; 20]).unwrap();
+
+        let state = SerializedGitGraphState {
+            log_source_type: Some(persistence::LOG_SOURCE_FILE),
+            log_source_value: Some("src/main.rs".to_string()),
+            log_order: Some(persistence::LOG_ORDER_TOPO),
+            selected_sha: Some(sha.to_string()),
+            search_query: Some("fix bug".to_string()),
+            search_case_sensitive: Some(true),
+        };
+
+        assert_eq!(
+            persistence::deserialize_log_source(&state),
+            LogSource::File(file_path)
+        );
+        assert!(matches!(
+            persistence::deserialize_log_order(&state),
+            LogOrder::TopoOrder
+        ));
+        assert_eq!(
+            state.selected_sha.as_deref(),
+            Some(sha.to_string()).as_deref()
+        );
+        assert_eq!(state.search_query.as_deref(), Some("fix bug"));
+        assert_eq!(state.search_case_sensitive, Some(true));
+
+        let all_state = SerializedGitGraphState {
+            log_source_type: Some(persistence::LOG_SOURCE_ALL),
+            log_source_value: None,
+            log_order: Some(persistence::LOG_ORDER_DATE),
+            selected_sha: None,
+            search_query: None,
+            search_case_sensitive: None,
+        };
+        assert_eq!(
+            persistence::deserialize_log_source(&all_state),
+            LogSource::All
+        );
+        assert!(matches!(
+            persistence::deserialize_log_order(&all_state),
+            LogOrder::DateOrder
+        ));
+
+        let branch_state = SerializedGitGraphState {
+            log_source_type: Some(persistence::LOG_SOURCE_BRANCH),
+            log_source_value: Some("refs/heads/main".to_string()),
+            ..Default::default()
+        };
+        assert_eq!(
+            persistence::deserialize_log_source(&branch_state),
+            LogSource::Branch("refs/heads/main".into())
+        );
+
+        let sha_state = SerializedGitGraphState {
+            log_source_type: Some(persistence::LOG_SOURCE_SHA),
+            log_source_value: Some(sha.to_string()),
+            ..Default::default()
+        };
+        assert_eq!(
+            persistence::deserialize_log_source(&sha_state),
+            LogSource::Sha(sha)
+        );
+
+        let empty_state = SerializedGitGraphState::default();
+        assert_eq!(
+            persistence::deserialize_log_source(&empty_state),
+            LogSource::All
+        );
+        assert!(matches!(
+            persistence::deserialize_log_order(&empty_state),
+            LogOrder::DateOrder
+        ));
+    }
+
+    #[gpui::test]
+    async fn test_git_graph_state_persists_across_serialization_roundtrip(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            Path::new("/project"),
+            json!({
+                ".git": {},
+                "file.txt": "content",
+            }),
+        )
+        .await;
+
+        let mut rng = StdRng::seed_from_u64(99);
+        let commits = generate_random_commit_dag(&mut rng, 20, false);
+        fs.set_graph_commits(Path::new("/project/.git"), commits.clone());
+
+        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 a repository")
+        });
+
+        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
+            workspace::MultiWorkspace::test_new(project.clone(), window, cx)
+        });
+        let workspace_weak =
+            multi_workspace.read_with(&*cx, |multi, _| multi.workspace().downgrade());
+
+        let git_graph = cx.new_window_entity(|window, cx| {
+            GitGraph::new(
+                repository.read(cx).id,
+                project.read(cx).git_store().clone(),
+                workspace_weak.clone(),
+                None,
+                window,
+                cx,
+            )
+        });
+        cx.run_until_parked();
+
+        cx.draw(
+            point(px(0.), px(0.)),
+            gpui::size(px(1200.), px(800.)),
+            |_, _| git_graph.clone().into_any_element(),
+        );
+        cx.run_until_parked();
+
+        let commit_count = git_graph.read_with(&*cx, |graph, _| graph.graph_data.commits.len());
+        assert!(commit_count > 0, "graph should have loaded commits, got 0");
+
+        let target_sha = commits[5].sha;
+        git_graph.update(cx, |graph, _| {
+            graph.selected_entry_idx = Some(5);
+        });
+
+        let selected_sha = git_graph.read_with(&*cx, |graph, _| {
+            graph
+                .selected_entry_idx
+                .and_then(|idx| graph.graph_data.commits.get(idx))
+                .map(|c| c.data.sha.to_string())
+        });
+        assert_eq!(selected_sha, Some(target_sha.to_string()));
+
+        let item_id = workspace::ItemId::from(999_u64);
+        let workspace_db = cx.read(|cx| workspace::WorkspaceDb::global(cx));
+        let workspace_id = workspace_db
+            .next_id()
+            .await
+            .expect("should create workspace id");
+        let db = cx.read(|cx| persistence::GitGraphsDb::global(cx));
+        db.save_git_graph(
+            item_id,
+            workspace_id,
+            "/project".to_string(),
+            Some(persistence::LOG_SOURCE_ALL),
+            None,
+            Some(persistence::LOG_ORDER_DATE),
+            selected_sha.clone(),
+            Some("some query".to_string()),
+            Some(true),
+        )
+        .await
+        .expect("save should succeed");
+
+        let restored_graph = cx
+            .update(|window, cx| {
+                <GitGraph as workspace::SerializableItem>::deserialize(
+                    project.clone(),
+                    workspace_weak,
+                    workspace_id,
+                    item_id,
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .expect("deserialization should succeed");
+        cx.run_until_parked();
+
+        cx.draw(
+            point(px(0.), px(0.)),
+            gpui::size(px(1200.), px(800.)),
+            |_, _| restored_graph.clone().into_any_element(),
+        );
+        cx.run_until_parked();
+
+        let restored_commit_count =
+            restored_graph.read_with(&*cx, |graph, _| graph.graph_data.commits.len());
+        assert_eq!(
+            restored_commit_count, commit_count,
+            "restored graph should have the same number of commits"
+        );
+
+        restored_graph.read_with(&*cx, |graph, _| {
+            assert_eq!(
+                graph.log_source,
+                LogSource::All,
+                "log_source should be restored"
+            );
+
+            let restored_selected_sha = graph
+                .selected_entry_idx
+                .and_then(|idx| graph.graph_data.commits.get(idx))
+                .map(|c| c.data.sha.to_string());
+            assert_eq!(
+                restored_selected_sha, selected_sha,
+                "selected commit should be restored via pending_select_sha"
+            );
+
+            assert_eq!(
+                graph.search_state.case_sensitive, true,
+                "search case sensitivity should be restored"
+            );
+        });
+
+        restored_graph.read_with(&*cx, |graph, cx| {
+            let editor_text = graph.search_state.editor.read(cx).text(cx);
+            assert_eq!(
+                editor_text, "some query",
+                "search query text should be restored in editor"
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_graph_data_reloaded_after_stash_change(cx: &mut TestAppContext) {
         init_test(cx);

crates/git_ui/src/file_history_view.rs 🔗

@@ -1,613 +0,0 @@
-use anyhow::Result;
-
-use git::repository::{FileHistory, FileHistoryEntry, RepoPath};
-use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
-use gpui::{
-    AnyElement, AnyEntity, App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
-    Render, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list,
-};
-use project::{
-    Project, ProjectPath,
-    git_store::{GitStore, Repository},
-};
-use std::any::{Any, TypeId};
-use std::sync::Arc;
-
-use time::OffsetDateTime;
-use ui::{Chip, Divider, ListItem, WithScrollbar, prelude::*};
-use util::ResultExt;
-use workspace::{
-    Item, Workspace,
-    item::{ItemEvent, SaveOptions},
-};
-
-use crate::commit_tooltip::CommitAvatar;
-use crate::commit_view::CommitView;
-
-const PAGE_SIZE: usize = 50;
-
-pub struct FileHistoryView {
-    history: FileHistory,
-    repository: WeakEntity<Repository>,
-    git_store: WeakEntity<GitStore>,
-    workspace: WeakEntity<Workspace>,
-    remote: Option<GitRemote>,
-    selected_entry: Option<usize>,
-    scroll_handle: UniformListScrollHandle,
-    focus_handle: FocusHandle,
-    loading_more: bool,
-    has_more: bool,
-}
-
-impl FileHistoryView {
-    pub fn open(
-        path: RepoPath,
-        git_store: WeakEntity<GitStore>,
-        repo: WeakEntity<Repository>,
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut App,
-    ) {
-        let file_history_task = git_store
-            .update(cx, |git_store, cx| {
-                repo.upgrade().map(|repo| {
-                    git_store.file_history_paginated(&repo, path.clone(), 0, Some(PAGE_SIZE), cx)
-                })
-            })
-            .ok()
-            .flatten();
-
-        window
-            .spawn(cx, async move |cx| {
-                let file_history = file_history_task?.await.log_err()?;
-                let repo = repo.upgrade()?;
-
-                workspace
-                    .update_in(cx, |workspace, window, cx| {
-                        let project = workspace.project();
-                        let view = cx.new(|cx| {
-                            FileHistoryView::new(
-                                file_history,
-                                git_store.clone(),
-                                repo.clone(),
-                                workspace.weak_handle(),
-                                project.clone(),
-                                window,
-                                cx,
-                            )
-                        });
-
-                        let pane = workspace.active_pane();
-                        pane.update(cx, |pane, cx| {
-                            let ix = pane.items().position(|item| {
-                                let view = item.downcast::<FileHistoryView>();
-                                view.is_some_and(|v| v.read(cx).history.path == path)
-                            });
-                            if let Some(ix) = ix {
-                                pane.activate_item(ix, true, true, window, cx);
-                            } else {
-                                pane.add_item(Box::new(view), true, true, None, window, cx);
-                            }
-                        })
-                    })
-                    .log_err()
-            })
-            .detach();
-    }
-
-    fn new(
-        history: FileHistory,
-        git_store: WeakEntity<GitStore>,
-        repository: Entity<Repository>,
-        workspace: WeakEntity<Workspace>,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let focus_handle = cx.focus_handle();
-        let scroll_handle = UniformListScrollHandle::new();
-        let has_more = history.entries.len() >= PAGE_SIZE;
-
-        let snapshot = repository.read(cx).snapshot();
-        let remote_url = snapshot
-            .remote_upstream_url
-            .as_ref()
-            .or(snapshot.remote_origin_url.as_ref());
-
-        let remote = remote_url.and_then(|url| {
-            let provider_registry = GitHostingProviderRegistry::default_global(cx);
-            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
-                host,
-                owner: parsed.owner.into(),
-                repo: parsed.repo.into(),
-            })
-        });
-
-        Self {
-            history,
-            git_store,
-            repository: repository.downgrade(),
-            workspace,
-            remote,
-            selected_entry: None,
-            scroll_handle,
-            focus_handle,
-            loading_more: false,
-            has_more,
-        }
-    }
-
-    fn load_more(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self.loading_more || !self.has_more {
-            return;
-        }
-
-        self.loading_more = true;
-        cx.notify();
-
-        let current_count = self.history.entries.len();
-        let path = self.history.path.clone();
-        let git_store = self.git_store.clone();
-        let repo = self.repository.clone();
-
-        let this = cx.weak_entity();
-        let task = window.spawn(cx, async move |cx| {
-            let file_history_task = git_store
-                .update(cx, |git_store, cx| {
-                    repo.upgrade().map(|repo| {
-                        git_store.file_history_paginated(
-                            &repo,
-                            path,
-                            current_count,
-                            Some(PAGE_SIZE),
-                            cx,
-                        )
-                    })
-                })
-                .ok()
-                .flatten();
-
-            if let Some(task) = file_history_task {
-                if let Ok(more_history) = task.await {
-                    this.update(cx, |this, cx| {
-                        this.loading_more = false;
-                        this.has_more = more_history.entries.len() >= PAGE_SIZE;
-                        this.history.entries.extend(more_history.entries);
-                        cx.notify();
-                    })
-                    .ok();
-                }
-            }
-        });
-
-        task.detach();
-    }
-
-    fn select_next(&mut self, _: &menu::SelectNext, _: &mut Window, cx: &mut Context<Self>) {
-        let entry_count = self.history.entries.len();
-        let ix = match self.selected_entry {
-            _ if entry_count == 0 => None,
-            None => Some(0),
-            Some(ix) => {
-                if ix == entry_count - 1 {
-                    Some(0)
-                } else {
-                    Some(ix + 1)
-                }
-            }
-        };
-        self.select_ix(ix, cx);
-    }
-
-    fn select_previous(
-        &mut self,
-        _: &menu::SelectPrevious,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let entry_count = self.history.entries.len();
-        let ix = match self.selected_entry {
-            _ if entry_count == 0 => None,
-            None => Some(entry_count - 1),
-            Some(ix) => {
-                if ix == 0 {
-                    Some(entry_count - 1)
-                } else {
-                    Some(ix - 1)
-                }
-            }
-        };
-        self.select_ix(ix, cx);
-    }
-
-    fn select_first(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
-        let entry_count = self.history.entries.len();
-        let ix = if entry_count != 0 { Some(0) } else { None };
-        self.select_ix(ix, cx);
-    }
-
-    fn select_last(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
-        let entry_count = self.history.entries.len();
-        let ix = if entry_count != 0 {
-            Some(entry_count - 1)
-        } else {
-            None
-        };
-        self.select_ix(ix, cx);
-    }
-
-    fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
-        self.selected_entry = ix;
-        if let Some(ix) = ix {
-            self.scroll_handle.scroll_to_item(ix, ScrollStrategy::Top);
-        }
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
-        self.open_commit_view(window, cx);
-    }
-
-    fn open_commit_view(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(entry) = self
-            .selected_entry
-            .and_then(|ix| self.history.entries.get(ix))
-        else {
-            return;
-        };
-
-        if let Some(repo) = self.repository.upgrade() {
-            let sha_str = entry.sha.to_string();
-            CommitView::open(
-                sha_str,
-                repo.downgrade(),
-                self.workspace.clone(),
-                None,
-                Some(self.history.path.clone()),
-                window,
-                cx,
-            );
-        }
-    }
-
-    fn render_commit_avatar(
-        &self,
-        sha: &SharedString,
-        author_email: Option<SharedString>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> AnyElement {
-        CommitAvatar::new(sha, author_email, self.remote.as_ref())
-            .size(rems_from_px(20.))
-            .render(window, cx)
-    }
-
-    fn render_commit_entry(
-        &self,
-        ix: usize,
-        entry: &FileHistoryEntry,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> AnyElement {
-        let pr_number = entry
-            .subject
-            .rfind("(#")
-            .and_then(|start| {
-                let rest = &entry.subject[start + 2..];
-                rest.find(')')
-                    .and_then(|end| rest[..end].parse::<u32>().ok())
-            })
-            .map(|num| format!("#{}", num))
-            .unwrap_or_else(|| {
-                if entry.sha.len() >= 7 {
-                    entry.sha[..7].to_string()
-                } else {
-                    entry.sha.to_string()
-                }
-            });
-
-        let commit_time = OffsetDateTime::from_unix_timestamp(entry.commit_timestamp)
-            .unwrap_or_else(|_| OffsetDateTime::UNIX_EPOCH);
-        let relative_timestamp = time_format::format_localized_timestamp(
-            commit_time,
-            OffsetDateTime::now_utc(),
-            time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC),
-            time_format::TimestampFormat::Relative,
-        );
-
-        ListItem::new(("commit", ix))
-            .toggle_state(Some(ix) == self.selected_entry)
-            .child(
-                h_flex()
-                    .h_8()
-                    .w_full()
-                    .pl_0p5()
-                    .pr_2p5()
-                    .gap_2()
-                    .child(
-                        div()
-                            .w(rems_from_px(52.))
-                            .flex_none()
-                            .child(Chip::new(pr_number)),
-                    )
-                    .child(self.render_commit_avatar(
-                        &entry.sha,
-                        Some(entry.author_email.clone()),
-                        window,
-                        cx,
-                    ))
-                    .child(
-                        h_flex()
-                            .min_w_0()
-                            .w_full()
-                            .justify_between()
-                            .child(
-                                h_flex()
-                                    .min_w_0()
-                                    .w_full()
-                                    .gap_1()
-                                    .child(
-                                        Label::new(entry.author_name.clone())
-                                            .size(LabelSize::Small)
-                                            .color(Color::Default)
-                                            .truncate(),
-                                    )
-                                    .child(
-                                        Label::new(&entry.subject)
-                                            .size(LabelSize::Small)
-                                            .color(Color::Muted)
-                                            .truncate(),
-                                    ),
-                            )
-                            .child(
-                                h_flex().flex_none().child(
-                                    Label::new(relative_timestamp)
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted),
-                                ),
-                            ),
-                    ),
-            )
-            .on_click(cx.listener(move |this, _, window, cx| {
-                this.selected_entry = Some(ix);
-                cx.notify();
-
-                this.open_commit_view(window, cx);
-            }))
-            .into_any_element()
-    }
-}
-
-impl EventEmitter<ItemEvent> for FileHistoryView {}
-
-impl Focusable for FileHistoryView {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for FileHistoryView {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let _file_name = self.history.path.file_name().unwrap_or("File");
-        let entry_count = self.history.entries.len();
-
-        v_flex()
-            .id("file_history_view")
-            .key_context("FileHistoryView")
-            .track_focus(&self.focus_handle)
-            .on_action(cx.listener(Self::select_next))
-            .on_action(cx.listener(Self::select_previous))
-            .on_action(cx.listener(Self::select_first))
-            .on_action(cx.listener(Self::select_last))
-            .on_action(cx.listener(Self::confirm))
-            .size_full()
-            .bg(cx.theme().colors().editor_background)
-            .child(
-                h_flex()
-                    .h(rems_from_px(41.))
-                    .pl_3()
-                    .pr_2()
-                    .justify_between()
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border_variant)
-                    .child(
-                        Label::new(self.history.path.as_unix_str().to_string())
-                            .color(Color::Muted)
-                            .buffer_font(cx),
-                    )
-                    .child(
-                        h_flex()
-                            .gap_1p5()
-                            .child(
-                                Label::new(format!("{} commits", entry_count))
-                                    .size(LabelSize::Small)
-                                    .color(Color::Muted)
-                                    .when(self.has_more, |this| this.mr_1()),
-                            )
-                            .when(self.has_more, |this| {
-                                this.child(Divider::vertical()).child(
-                                    Button::new("load-more", "Load More")
-                                        .disabled(self.loading_more)
-                                        .label_size(LabelSize::Small)
-                                        .start_icon(
-                                            Icon::new(IconName::ArrowCircle)
-                                                .size(IconSize::Small)
-                                                .color(Color::Muted),
-                                        )
-                                        .on_click(cx.listener(|this, _, window, cx| {
-                                            this.load_more(window, cx);
-                                        })),
-                                )
-                            }),
-                    ),
-            )
-            .child(
-                v_flex()
-                    .flex_1()
-                    .size_full()
-                    .child({
-                        let view = cx.weak_entity();
-                        uniform_list(
-                            "file-history-list",
-                            entry_count,
-                            move |range, window, cx| {
-                                let Some(view) = view.upgrade() else {
-                                    return Vec::new();
-                                };
-                                view.update(cx, |this, cx| {
-                                    let mut items = Vec::with_capacity(range.end - range.start);
-                                    for ix in range {
-                                        if let Some(entry) = this.history.entries.get(ix) {
-                                            items.push(
-                                                this.render_commit_entry(ix, entry, window, cx),
-                                            );
-                                        }
-                                    }
-                                    items
-                                })
-                            },
-                        )
-                        .flex_1()
-                        .size_full()
-                        .track_scroll(&self.scroll_handle)
-                    })
-                    .vertical_scrollbar_for(&self.scroll_handle, window, cx),
-            )
-    }
-}
-
-impl Item for FileHistoryView {
-    type Event = ItemEvent;
-
-    fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(ItemEvent)) {
-        f(*event)
-    }
-
-    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
-        let file_name = self
-            .history
-            .path
-            .file_name()
-            .map(|name| name.to_string())
-            .unwrap_or_else(|| "File".to_string());
-        format!("History: {}", file_name).into()
-    }
-
-    fn tab_tooltip_text(&self, _cx: &App) -> Option<SharedString> {
-        Some(format!("Git history for {}", self.history.path.as_unix_str()).into())
-    }
-
-    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
-        Some(Icon::new(IconName::GitBranch))
-    }
-
-    fn telemetry_event_text(&self) -> Option<&'static str> {
-        Some("file history")
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: Option<workspace::WorkspaceId>,
-        _window: &mut Window,
-        _cx: &mut Context<Self>,
-    ) -> Task<Option<Entity<Self>>> {
-        Task::ready(None)
-    }
-
-    fn navigate(
-        &mut self,
-        _: Arc<dyn Any + Send>,
-        _window: &mut Window,
-        _: &mut Context<Self>,
-    ) -> bool {
-        false
-    }
-
-    fn deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
-
-    fn can_save(&self, _: &App) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _options: SaveOptions,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        _: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Ok(()))
-    }
-
-    fn save_as(
-        &mut self,
-        _project: Entity<Project>,
-        _path: ProjectPath,
-        _window: &mut Window,
-        _: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Ok(()))
-    }
-
-    fn reload(
-        &mut self,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        _: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Ok(()))
-    }
-
-    fn is_dirty(&self, _: &App) -> bool {
-        false
-    }
-
-    fn has_conflict(&self, _: &App) -> bool {
-        false
-    }
-
-    fn breadcrumbs(
-        &self,
-        _cx: &App,
-    ) -> Option<(Vec<workspace::item::HighlightedText>, Option<gpui::Font>)> {
-        None
-    }
-
-    fn added_to_workspace(
-        &mut self,
-        _workspace: &mut Workspace,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        window.focus(&self.focus_handle, cx);
-    }
-
-    fn show_toolbar(&self) -> bool {
-        true
-    }
-
-    fn pixel_position_of_cursor(&self, _: &App) -> Option<gpui::Point<gpui::Pixels>> {
-        None
-    }
-
-    fn set_nav_history(
-        &mut self,
-        _: workspace::ItemNavHistory,
-        _window: &mut Window,
-        _: &mut Context<Self>,
-    ) {
-    }
-
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a Entity<Self>,
-        _: &'a App,
-    ) -> Option<AnyEntity> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle.clone().into())
-        } else {
-            None
-        }
-    }
-}

crates/git_ui/src/git_ui.rs 🔗

@@ -33,7 +33,6 @@ pub mod commit_tooltip;
 pub mod commit_view;
 mod conflict_view;
 pub mod file_diff_view;
-pub mod file_history_view;
 pub mod git_panel;
 mod git_panel_settings;
 pub mod git_picker;

crates/project/src/git_store.rs 🔗

@@ -573,7 +573,6 @@ impl GitStore {
         client.add_entity_request_handler(Self::handle_compare_checkpoints);
         client.add_entity_request_handler(Self::handle_diff_checkpoints);
         client.add_entity_request_handler(Self::handle_load_commit_diff);
-        client.add_entity_request_handler(Self::handle_file_history);
         client.add_entity_request_handler(Self::handle_checkout_files);
         client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
         client.add_entity_request_handler(Self::handle_set_index_text);
@@ -1218,30 +1217,6 @@ impl GitStore {
         })
     }
 
-    pub fn file_history(
-        &self,
-        repo: &Entity<Repository>,
-        path: RepoPath,
-        cx: &mut App,
-    ) -> Task<Result<git::repository::FileHistory>> {
-        let rx = repo.update(cx, |repo, _| repo.file_history(path));
-
-        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
-    }
-
-    pub fn file_history_paginated(
-        &self,
-        repo: &Entity<Repository>,
-        path: RepoPath,
-        skip: usize,
-        limit: Option<usize>,
-        cx: &mut App,
-    ) -> Task<Result<git::repository::FileHistory>> {
-        let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit));
-
-        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
-    }
-
     pub fn get_permalink_to_line(
         &self,
         buffer: &Entity<Buffer>,
@@ -2767,40 +2742,6 @@ impl GitStore {
         })
     }
 
-    async fn handle_file_history(
-        this: Entity<Self>,
-        envelope: TypedEnvelope<proto::GitFileHistory>,
-        mut cx: AsyncApp,
-    ) -> Result<proto::GitFileHistoryResponse> {
-        let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
-        let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
-        let path = RepoPath::from_proto(&envelope.payload.path)?;
-        let skip = envelope.payload.skip as usize;
-        let limit = envelope.payload.limit.map(|l| l as usize);
-
-        let file_history = repository_handle
-            .update(&mut cx, |repository_handle, _| {
-                repository_handle.file_history_paginated(path, skip, limit)
-            })
-            .await??;
-
-        Ok(proto::GitFileHistoryResponse {
-            entries: file_history
-                .entries
-                .into_iter()
-                .map(|entry| proto::FileHistoryEntry {
-                    sha: entry.sha.to_string(),
-                    subject: entry.subject.to_string(),
-                    message: entry.message.to_string(),
-                    commit_timestamp: entry.commit_timestamp,
-                    author_name: entry.author_name.to_string(),
-                    author_email: entry.author_email.to_string(),
-                })
-                .collect(),
-            path: file_history.path.to_proto(),
-        })
-    }
-
     async fn handle_reset(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::GitReset>,
@@ -4079,14 +4020,15 @@ impl Repository {
             })
             .shared();
 
+        // todo(git_graph_remote): Make this subscription on both remote/local repo
         cx.subscribe_self(move |this, event: &RepositoryEvent, _| match event {
             RepositoryEvent::HeadChanged | RepositoryEvent::BranchListChanged => {
-                if this.scan_id > 1 {
+                if this.scan_id > 2 {
                     this.initial_graph_data.clear();
                 }
             }
             RepositoryEvent::StashEntriesChanged => {
-                if this.scan_id > 1 {
+                if this.scan_id > 2 {
                     this.initial_graph_data
                         .retain(|(log_source, _), _| *log_source != LogSource::All);
                 }
@@ -4667,55 +4609,6 @@ impl Repository {
         })
     }
 
-    pub fn file_history(
-        &mut self,
-        path: RepoPath,
-    ) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
-        self.file_history_paginated(path, 0, None)
-    }
-
-    pub fn file_history_paginated(
-        &mut self,
-        path: RepoPath,
-        skip: usize,
-        limit: Option<usize>,
-    ) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
-        let id = self.id;
-        self.send_job(None, move |git_repo, _cx| async move {
-            match git_repo {
-                RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
-                    backend.file_history_paginated(path, skip, limit).await
-                }
-                RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
-                    let response = client
-                        .request(proto::GitFileHistory {
-                            project_id: project_id.0,
-                            repository_id: id.to_proto(),
-                            path: path.to_proto(),
-                            skip: skip as u64,
-                            limit: limit.map(|l| l as u64),
-                        })
-                        .await?;
-                    Ok(git::repository::FileHistory {
-                        entries: response
-                            .entries
-                            .into_iter()
-                            .map(|entry| git::repository::FileHistoryEntry {
-                                sha: entry.sha.into(),
-                                subject: entry.subject.into(),
-                                message: entry.message.into(),
-                                commit_timestamp: entry.commit_timestamp,
-                                author_name: entry.author_name.into(),
-                                author_email: entry.author_email.into(),
-                            })
-                            .collect(),
-                        path: RepoPath::from_proto(&response.path)?,
-                    })
-                }
-            }
-        })
-    }
-
     pub fn get_graph_data(
         &self,
         log_source: LogSource,

crates/proto/proto/git.proto 🔗

@@ -310,29 +310,6 @@ message GitCheckoutFiles {
   repeated string paths = 5;
 }
 
-message GitFileHistory {
-  uint64 project_id = 1;
-  reserved 2;
-  uint64 repository_id = 3;
-  string path = 4;
-  uint64 skip = 5;
-  optional uint64 limit = 6;
-}
-
-message GitFileHistoryResponse {
-  repeated FileHistoryEntry entries = 1;
-  string path = 2;
-}
-
-message FileHistoryEntry {
-  string sha = 1;
-  string subject = 2;
-  string message = 3;
-  int64 commit_timestamp = 4;
-  string author_name = 5;
-  string author_email = 6;
-}
-
 // Move to `git.proto` once collab's min version is >=0.171.0.
 message StatusEntry {
   string repo_path = 1;

crates/proto/proto/zed.proto 🔗

@@ -423,8 +423,7 @@ message Envelope {
     OpenImageResponse open_image_response = 392;
     CreateImageForPeer create_image_for_peer = 393;
 
-    GitFileHistory git_file_history = 397;
-    GitFileHistoryResponse git_file_history_response = 398;
+    // 397-398 reserved (was GitFileHistory)
 
     RunGitHook run_git_hook = 399;
 

crates/proto/src/proto.rs 🔗

@@ -292,8 +292,6 @@ messages!(
     (GitCheckoutFiles, Background),
     (GitShow, Background),
     (GitCommitDetails, Background),
-    (GitFileHistory, Background),
-    (GitFileHistoryResponse, Background),
     (GitCreateCheckpoint, Background),
     (GitCreateCheckpointResponse, Background),
     (GitRestoreCheckpoint, Background),
@@ -522,7 +520,6 @@ request_messages!(
     (InstallExtension, Ack),
     (RegisterBufferWithLanguageServers, Ack),
     (GitShow, GitCommitDetails),
-    (GitFileHistory, GitFileHistoryResponse),
     (GitCreateCheckpoint, GitCreateCheckpointResponse),
     (GitRestoreCheckpoint, Ack),
     (GitCompareCheckpoints, GitCompareCheckpointsResponse),
@@ -709,7 +706,6 @@ entity_messages!(
     CancelLanguageServerWork,
     RegisterBufferWithLanguageServers,
     GitShow,
-    GitFileHistory,
     GitCreateCheckpoint,
     GitRestoreCheckpoint,
     GitCompareCheckpoints,