Rework shared commit editors (#24274)

Kirill Bulatov , Conrad Irwin , d1y , and Smit created

Rework of https://github.com/zed-industries/zed/pull/24130
Uses
https://github.com/d1y/git_firefly/tree/1033c0b57ec88a002cb68efc64c8d9bf5c212e30
`COMMIT_EDITMSG` language-related definitions (thanks @d1y )

Instead of using real `.git/COMMIT_EDITMSG` file, create a buffer
without FS representation, stored in the `Repository` and shared the
regular way via the `BufferStore`.
Adds a knowledge of what `Git Commit` language is, and uses it in the
buffers which are rendered in the git panel.


Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: d1y <chenhonzhou@gmail.com>
Co-authored-by: Smit <smit@zed.dev>

Change summary

Cargo.lock                                    |  11 +
Cargo.toml                                    |   1 
crates/git/src/repository.rs                  |  18 -
crates/git_ui/Cargo.toml                      |   1 
crates/git_ui/src/git_panel.rs                | 198 ++++++++------------
crates/git_ui/src/project_diff.rs             |  75 ++++---
crates/git_ui/src/repository_selector.rs      |  12 
crates/languages/Cargo.toml                   |   2 
crates/languages/src/gitcommit/config.toml    |  18 +
crates/languages/src/gitcommit/highlights.scm |  18 +
crates/languages/src/gitcommit/injections.scm |   5 
crates/languages/src/lib.rs                   |  20 ++
crates/project/src/git.rs                     | 204 ++++++++++++++------
crates/project/src/project.rs                 | 126 ++++--------
crates/proto/proto/zed.proto                  |   1 
crates/remote_server/src/headless_project.rs  |  86 ++-----
crates/worktree/src/worktree.rs               |   2 
17 files changed, 427 insertions(+), 371 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5254,7 +5254,6 @@ dependencies = [
  "picker",
  "postage",
  "project",
- "rpc",
  "schemars",
  "serde",
  "serde_derive",
@@ -7008,6 +7007,7 @@ dependencies = [
  "tree-sitter-cpp",
  "tree-sitter-css",
  "tree-sitter-diff",
+ "tree-sitter-gitcommit",
  "tree-sitter-go",
  "tree-sitter-gomod",
  "tree-sitter-gowork",
@@ -13944,6 +13944,15 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "tree-sitter-gitcommit"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/tree-sitter-git-commit?rev=88309716a69dd13ab83443721ba6e0b491d37ee9#88309716a69dd13ab83443721ba6e0b491d37ee9"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "tree-sitter-go"
 version = "0.23.4"

Cargo.toml 🔗

@@ -522,6 +522,7 @@ tree-sitter-cpp = "0.23"
 tree-sitter-css = "0.23"
 tree-sitter-elixir = "0.3"
 tree-sitter-embedded-template = "0.23.0"
+tree-sitter-gitcommit = {git  = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"}
 tree-sitter-go = "0.23"
 tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
 tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }

crates/git/src/repository.rs 🔗

@@ -1,6 +1,6 @@
 use crate::status::FileStatus;
+use crate::GitHostingProviderRegistry;
 use crate::{blame::Blame, status::GitStatus};
-use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE};
 use anyhow::{anyhow, Context as _, Result};
 use collections::{HashMap, HashSet};
 use git2::BranchType;
@@ -68,7 +68,7 @@ pub trait GitRepository: Send + Sync {
     /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
     fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
 
-    fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()>;
+    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -298,22 +298,14 @@ impl GitRepository for RealGitRepository {
         Ok(())
     }
 
-    fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
         let working_directory = self
             .repository
             .lock()
             .workdir()
             .context("failed to read git work directory")?
             .to_path_buf();
-        let commit_file = self.dot_git_dir().join(*COMMIT_MESSAGE);
-        let commit_file_path = commit_file.to_string_lossy();
-        let mut args = vec![
-            "commit",
-            "--quiet",
-            "-F",
-            commit_file_path.as_ref(),
-            "--cleanup=strip",
-        ];
+        let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
         let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
         if let Some(author) = author.as_deref() {
             args.push("--author");
@@ -480,7 +472,7 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 
-    fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
         unimplemented!()
     }
 }

crates/git_ui/Cargo.toml 🔗

@@ -26,7 +26,6 @@ multi_buffer.workspace = true
 menu.workspace = true
 postage.workspace = true
 project.workspace = true
-rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true

crates/git_ui/src/git_panel.rs 🔗

@@ -4,7 +4,7 @@ use crate::ProjectDiff;
 use crate::{
     git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
 };
-use anyhow::{Context as _, Result};
+use anyhow::Result;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::actions::MoveToEnd;
@@ -12,13 +12,12 @@ use editor::scroll::ScrollbarAutoHide;
 use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
 use git::repository::RepoPath;
 use git::status::FileStatus;
-use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE};
+use git::{CommitAllChanges, CommitChanges, ToggleStaged};
 use gpui::*;
-use language::{Buffer, BufferId};
+use language::Buffer;
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use project::git::{GitEvent, GitRepo, RepositoryHandle};
-use project::{CreateOptions, Fs, Project, ProjectPath};
-use rpc::proto;
+use project::git::{GitEvent, Repository};
+use project::{Fs, Project, ProjectPath};
 use serde::{Deserialize, Serialize};
 use settings::Settings as _;
 use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
@@ -32,7 +31,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
 use workspace::Toast;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
-    Item, Workspace,
+    Workspace,
 };
 
 actions!(
@@ -144,7 +143,7 @@ pub struct GitPanel {
     pending_serialization: Task<Option<()>>,
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
-    active_repository: Option<RepositoryHandle>,
+    active_repository: Option<Entity<Repository>>,
     scroll_handle: UniformListScrollHandle,
     scrollbar_state: ScrollbarState,
     selected_entry: Option<usize>,
@@ -162,63 +161,6 @@ pub struct GitPanel {
     can_commit_all: bool,
 }
 
-fn commit_message_buffer(
-    project: &Entity<Project>,
-    active_repository: &RepositoryHandle,
-    cx: &mut App,
-) -> Task<Result<Entity<Buffer>>> {
-    match &active_repository.git_repo {
-        GitRepo::Local(repo) => {
-            let commit_message_file = repo.dot_git_dir().join(*COMMIT_MESSAGE);
-            let fs = project.read(cx).fs().clone();
-            let project = project.downgrade();
-            cx.spawn(|mut cx| async move {
-                fs.create_file(
-                    &commit_message_file,
-                    CreateOptions {
-                        overwrite: false,
-                        ignore_if_exists: true,
-                    },
-                )
-                .await
-                .with_context(|| format!("creating commit message file {commit_message_file:?}"))?;
-                let buffer = project
-                    .update(&mut cx, |project, cx| {
-                        project.open_local_buffer(&commit_message_file, cx)
-                    })?
-                    .await
-                    .with_context(|| {
-                        format!("opening commit message buffer at {commit_message_file:?}",)
-                    })?;
-                Ok(buffer)
-            })
-        }
-        GitRepo::Remote {
-            project_id,
-            client,
-            worktree_id,
-            work_directory_id,
-        } => {
-            let request = client.request(proto::OpenCommitMessageBuffer {
-                project_id: project_id.0,
-                worktree_id: worktree_id.to_proto(),
-                work_directory_id: work_directory_id.to_proto(),
-            });
-            let project = project.downgrade();
-            cx.spawn(|mut cx| async move {
-                let response = request.await.context("requesting to open commit buffer")?;
-                let buffer_id = BufferId::new(response.buffer_id)?;
-                let buffer = project
-                    .update(&mut cx, {
-                        |project, cx| project.wait_for_remote_buffer(buffer_id, cx)
-                    })?
-                    .await?;
-                Ok(buffer)
-            })
-        }
-    }
-}
-
 fn commit_message_editor(
     commit_message_buffer: Option<Entity<Buffer>>,
     window: &mut Window,
@@ -360,7 +302,7 @@ impl GitPanel {
         let Some(git_repo) = self.active_repository.as_ref() else {
             return;
         };
-        let Some(repo_path) = git_repo.project_path_to_repo_path(&path) else {
+        let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
             return;
         };
         let Some(ix) = self.entries_by_path.get(&repo_path) else {
@@ -578,7 +520,7 @@ impl GitPanel {
             .active_repository
             .as_ref()
             .map_or(false, |active_repository| {
-                active_repository.entry_count() > 0
+                active_repository.read(cx).entry_count() > 0
             });
         if have_entries && self.selected_entry.is_none() {
             self.selected_entry = Some(0);
@@ -655,11 +597,17 @@ impl GitPanel {
             let repo_paths = repo_paths.clone();
             let active_repository = active_repository.clone();
             |this, mut cx| async move {
-                let result = if stage {
-                    active_repository.stage_entries(repo_paths.clone()).await
-                } else {
-                    active_repository.unstage_entries(repo_paths.clone()).await
-                };
+                let result = cx
+                    .update(|cx| {
+                        if stage {
+                            active_repository.read(cx).stage_entries(repo_paths.clone())
+                        } else {
+                            active_repository
+                                .read(cx)
+                                .unstage_entries(repo_paths.clone())
+                        }
+                    })?
+                    .await?;
 
                 this.update(&mut cx, |this, cx| {
                     for pending in this.pending.iter_mut() {
@@ -697,7 +645,9 @@ impl GitPanel {
         let Some(active_repository) = self.active_repository.as_ref() else {
             return;
         };
-        let Some(path) = active_repository.repo_path_to_project_path(&status_entry.repo_path)
+        let Some(path) = active_repository
+            .read(cx)
+            .repo_path_to_project_path(&status_entry.repo_path)
         else {
             return;
         };
@@ -725,18 +675,18 @@ impl GitPanel {
         if !self.can_commit {
             return;
         }
-        if self.commit_editor.read(cx).is_empty(cx) {
+        let message = self.commit_editor.read(cx).text(cx);
+        if message.trim().is_empty() {
             return;
         }
         self.commit_pending = true;
-        let save_task = self.commit_editor.update(cx, |editor, cx| {
-            editor.save(false, self.project.clone(), window, cx)
-        });
         let commit_editor = self.commit_editor.clone();
         self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
+            let commit = active_repository.update(&mut cx, |active_repository, _| {
+                active_repository.commit(SharedString::from(message), name_and_email)
+            })?;
             let result = maybe!(async {
-                save_task.await?;
-                active_repository.commit(name_and_email).await?;
+                commit.await??;
                 cx.update(|window, cx| {
                     commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
                 })
@@ -768,14 +718,12 @@ impl GitPanel {
         if !self.can_commit_all {
             return;
         }
-        if self.commit_editor.read(cx).is_empty(cx) {
+
+        let message = self.commit_editor.read(cx).text(cx);
+        if message.trim().is_empty() {
             return;
         }
         self.commit_pending = true;
-        let save_task = self.commit_editor.update(cx, |editor, cx| {
-            editor.save(false, self.project.clone(), window, cx)
-        });
-
         let commit_editor = self.commit_editor.clone();
         let tracked_files = self
             .entries
@@ -790,9 +738,15 @@ impl GitPanel {
 
         self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
             let result = maybe!(async {
-                save_task.await?;
-                active_repository.stage_entries(tracked_files).await?;
-                active_repository.commit(name_and_email).await
+                cx.update(|_, cx| active_repository.read(cx).stage_entries(tracked_files))?
+                    .await??;
+                cx.update(|_, cx| {
+                    active_repository
+                        .read(cx)
+                        .commit(SharedString::from(message), name_and_email)
+                })?
+                .await??;
+                Ok(())
             })
             .await;
             cx.update(|window, cx| match result {
@@ -886,47 +840,56 @@ impl GitPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let project = self.project.clone();
         let handle = cx.entity().downgrade();
+        self.reopen_commit_buffer(window, cx);
         self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
             cx.background_executor().timer(UPDATE_DEBOUNCE).await;
             if let Some(git_panel) = handle.upgrade() {
-                let Ok(commit_message_buffer) = git_panel.update_in(&mut cx, |git_panel, _, cx| {
-                    git_panel
-                        .active_repository
-                        .as_ref()
-                        .map(|active_repository| {
-                            commit_message_buffer(&project, active_repository, cx)
-                        })
-                }) else {
-                    return;
-                };
-                let commit_message_buffer = match commit_message_buffer {
-                    Some(commit_message_buffer) => match commit_message_buffer
-                        .await
-                        .context("opening commit buffer on repo update")
-                        .log_err()
-                    {
-                        Some(buffer) => Some(buffer),
-                        None => return,
-                    },
-                    None => None,
-                };
-
                 git_panel
-                    .update_in(&mut cx, |git_panel, window, cx| {
-                        git_panel.update_visible_entries(cx);
+                    .update_in(&mut cx, |git_panel, _, cx| {
                         if clear_pending {
                             git_panel.clear_pending();
                         }
-                        git_panel.commit_editor =
-                            cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+                        git_panel.update_visible_entries(cx);
                     })
                     .ok();
             }
         });
     }
 
+    fn reopen_commit_buffer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(active_repo) = self.active_repository.as_ref() else {
+            return;
+        };
+        let load_buffer = active_repo.update(cx, |active_repo, cx| {
+            let project = self.project.read(cx);
+            active_repo.open_commit_buffer(
+                Some(project.languages().clone()),
+                project.buffer_store().clone(),
+                cx,
+            )
+        });
+
+        cx.spawn_in(window, |git_panel, mut cx| async move {
+            let buffer = load_buffer.await?;
+            git_panel.update_in(&mut cx, |git_panel, window, cx| {
+                if git_panel
+                    .commit_editor
+                    .read(cx)
+                    .buffer()
+                    .read(cx)
+                    .as_singleton()
+                    .as_ref()
+                    != Some(&buffer)
+                {
+                    git_panel.commit_editor =
+                        cx.new(|cx| commit_message_editor(Some(buffer), window, cx));
+                }
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn clear_pending(&mut self) {
         self.pending.retain(|v| !v.finished)
     }
@@ -944,6 +907,7 @@ impl GitPanel {
         };
 
         // First pass - collect all paths
+        let repo = repo.read(cx);
         let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
 
         let mut has_changed_checked_boxes = false;
@@ -1117,7 +1081,7 @@ impl GitPanel {
         let entry_count = self
             .active_repository
             .as_ref()
-            .map_or(0, RepositoryHandle::entry_count);
+            .map_or(0, |repo| repo.read(cx).entry_count());
 
         let changes_string = match entry_count {
             0 => "No changes".to_string(),
@@ -1151,7 +1115,7 @@ impl GitPanel {
         let active_repository = self.project.read(cx).active_repository(cx);
         let repository_display_name = active_repository
             .as_ref()
-            .map(|repo| repo.display_name(self.project.read(cx), cx))
+            .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx))
             .unwrap_or_default();
 
         let entry_count = self.entries.len();
@@ -1619,7 +1583,7 @@ impl Render for GitPanel {
             .active_repository
             .as_ref()
             .map_or(false, |active_repository| {
-                active_repository.entry_count() > 0
+                active_repository.read(cx).entry_count() > 0
             });
         let room = self
             .workspace

crates/git_ui/src/project_diff.rs 🔗

@@ -163,6 +163,7 @@ impl ProjectDiff {
         };
 
         let Some(path) = git_repo
+            .read(cx)
             .repo_path_to_project_path(&entry.repo_path)
             .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx))
         else {
@@ -234,43 +235,45 @@ impl ProjectDiff {
         let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
 
         let mut result = vec![];
-        for entry in repo.status() {
-            if !entry.status.has_changes() {
-                continue;
+        repo.update(cx, |repo, cx| {
+            for entry in repo.status() {
+                if !entry.status.has_changes() {
+                    continue;
+                }
+                let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
+                    continue;
+                };
+                let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
+                    continue;
+                };
+                // Craft some artificial paths so that created entries will appear last.
+                let path_key = if entry.status.is_created() {
+                    PathKey::namespaced(ADDED_NAMESPACE, &abs_path)
+                } else {
+                    PathKey::namespaced(CHANGED_NAMESPACE, &abs_path)
+                };
+
+                previous_paths.remove(&path_key);
+                let load_buffer = self
+                    .project
+                    .update(cx, |project, cx| project.open_buffer(project_path, cx));
+
+                let project = self.project.clone();
+                result.push(cx.spawn(|_, mut cx| async move {
+                    let buffer = load_buffer.await?;
+                    let changes = project
+                        .update(&mut cx, |project, cx| {
+                            project.open_uncommitted_changes(buffer.clone(), cx)
+                        })?
+                        .await?;
+                    Ok(DiffBuffer {
+                        path_key,
+                        buffer,
+                        change_set: changes,
+                    })
+                }));
             }
-            let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
-                continue;
-            };
-            let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
-                continue;
-            };
-            // Craft some artificial paths so that created entries will appear last.
-            let path_key = if entry.status.is_created() {
-                PathKey::namespaced(ADDED_NAMESPACE, &abs_path)
-            } else {
-                PathKey::namespaced(CHANGED_NAMESPACE, &abs_path)
-            };
-
-            previous_paths.remove(&path_key);
-            let load_buffer = self
-                .project
-                .update(cx, |project, cx| project.open_buffer(project_path, cx));
-
-            let project = self.project.clone();
-            result.push(cx.spawn(|_, mut cx| async move {
-                let buffer = load_buffer.await?;
-                let changes = project
-                    .update(&mut cx, |project, cx| {
-                        project.open_uncommitted_changes(buffer.clone(), cx)
-                    })?
-                    .await?;
-                Ok(DiffBuffer {
-                    path_key,
-                    buffer,
-                    change_set: changes,
-                })
-            }));
-        }
+        });
         self.multibuffer.update(cx, |multibuffer, cx| {
             for path in previous_paths {
                 multibuffer.remove_excerpts_for_path(path, cx);

crates/git_ui/src/repository_selector.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 use project::{
-    git::{GitState, RepositoryHandle},
+    git::{GitState, Repository},
     Project,
 };
 use std::sync::Arc;
@@ -117,13 +117,13 @@ impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
 pub struct RepositorySelectorDelegate {
     project: WeakEntity<Project>,
     repository_selector: WeakEntity<RepositorySelector>,
-    repository_entries: Vec<RepositoryHandle>,
-    filtered_repositories: Vec<RepositoryHandle>,
+    repository_entries: Vec<Entity<Repository>>,
+    filtered_repositories: Vec<Entity<Repository>>,
     selected_index: usize,
 }
 
 impl RepositorySelectorDelegate {
-    pub fn update_repository_entries(&mut self, all_repositories: Vec<RepositoryHandle>) {
+    pub fn update_repository_entries(&mut self, all_repositories: Vec<Entity<Repository>>) {
         self.repository_entries = all_repositories.clone();
         self.filtered_repositories = all_repositories;
         self.selected_index = 0;
@@ -194,7 +194,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
         let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
             return;
         };
-        selected_repo.activate(cx);
+        selected_repo.update(cx, |selected_repo, cx| selected_repo.activate(cx));
         self.dismissed(window, cx);
     }
 
@@ -222,7 +222,7 @@ impl PickerDelegate for RepositorySelectorDelegate {
     ) -> Option<Self::ListItem> {
         let project = self.project.upgrade()?;
         let repo_info = self.filtered_repositories.get(ix)?;
-        let display_name = repo_info.display_name(project.read(cx), cx);
+        let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
         // TODO: Implement repository item rendering
         Some(
             ListItem::new(ix)

crates/languages/Cargo.toml 🔗

@@ -19,6 +19,7 @@ load-grammars = [
     "tree-sitter-cpp",
     "tree-sitter-css",
     "tree-sitter-diff",
+    "tree-sitter-gitcommit",
     "tree-sitter-go",
     "tree-sitter-go-mod",
     "tree-sitter-gowork",
@@ -69,6 +70,7 @@ tree-sitter-c = { workspace = true, optional = true }
 tree-sitter-cpp = { workspace = true, optional = true }
 tree-sitter-css = { workspace = true, optional = true }
 tree-sitter-diff = { workspace = true, optional = true }
+tree-sitter-gitcommit = {workspace = true, optional = true }
 tree-sitter-go = { workspace = true, optional = true }
 tree-sitter-go-mod = { workspace = true, optional = true }
 tree-sitter-gowork = { workspace = true, optional = true }

crates/languages/src/gitcommit/config.toml 🔗

@@ -0,0 +1,18 @@
+name = "Git Commit"
+grammar = "git_commit"
+path_suffixes = [
+  "TAG_EDITMSG",
+  "MERGE_MSG",
+  "COMMIT_EDITMSG",
+  "NOTES_EDITMSG",
+  "EDIT_DESCRIPTION",
+]
+line_comments = ["#"]
+brackets = [
+  { start = "(", end = ")", close = true, newline = false },
+  { start = "`", end = "`", close = true, newline = false },
+  { start = "\"", end = "\"", close = true, newline = false },
+  { start = "'", end = "'", close = true, newline = false },
+  { start = "{", end = "}", close = true, newline = false },
+  { start = "[", end = "]", close = true, newline = false },
+]

crates/languages/src/gitcommit/highlights.scm 🔗

@@ -0,0 +1,18 @@
+(subject) @markup.heading
+(path) @string.special.path
+(branch) @string.special.symbol
+(commit) @constant
+(item) @markup.link.url
+(header) @tag
+
+(change kind: "new file" @diff.plus)
+(change kind: "deleted" @diff.minus)
+(change kind: "modified" @diff.delta)
+(change kind: "renamed" @diff.delta.moved)
+
+(trailer
+  key: (trailer_key) @variable.other.member
+  value: (trailer_value) @string)
+
+[":" "=" "->" (scissors)] @punctuation.delimiter
+(comment) @comment

crates/languages/src/lib.rs 🔗

@@ -31,6 +31,25 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
+/// A shared grammar for plain text, exposed for reuse by downstream crates.
+#[cfg(feature = "tree-sitter-gitcommit")]
+pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
+    std::sync::LazyLock::new(|| {
+        Arc::new(Language::new(
+            LanguageConfig {
+                name: "Git Commit".into(),
+                soft_wrap: Some(language::language_settings::SoftWrap::EditorWidth),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["COMMIT_EDITMSG".to_owned()],
+                    first_line_pattern: None,
+                },
+                line_comments: vec![Arc::from("#")],
+                ..LanguageConfig::default()
+            },
+            Some(tree_sitter_gitcommit::LANGUAGE.into()),
+        ))
+    });
+
 pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mut App) {
     #[cfg(feature = "load-grammars")]
     languages.register_native_grammars([
@@ -53,6 +72,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
         ("tsx", tree_sitter_typescript::LANGUAGE_TSX),
         ("typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
         ("yaml", tree_sitter_yaml::LANGUAGE),
+        ("gitcommit", tree_sitter_gitcommit::LANGUAGE),
     ]);
 
     macro_rules! language {

crates/project/src/git.rs 🔗

@@ -1,6 +1,7 @@
+use crate::buffer_store::BufferStore;
 use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
 use crate::{Project, ProjectPath};
-use anyhow::{anyhow, Context as _};
+use anyhow::Context as _;
 use client::ProjectId;
 use futures::channel::{mpsc, oneshot};
 use futures::StreamExt as _;
@@ -8,24 +9,28 @@ use git::{
     repository::{GitRepository, RepoPath},
     status::{GitSummary, TrackedSummary},
 };
-use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
+use gpui::{
+    App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity,
+};
+use language::{Buffer, LanguageRegistry};
 use rpc::{proto, AnyProtoClient};
 use settings::WorktreeId;
 use std::sync::Arc;
+use text::BufferId;
 use util::{maybe, ResultExt};
 use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
 
 pub struct GitState {
     project_id: Option<ProjectId>,
     client: Option<AnyProtoClient>,
-    repositories: Vec<RepositoryHandle>,
+    repositories: Vec<Entity<Repository>>,
     active_index: Option<usize>,
     update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
     _subscription: Subscription,
 }
 
-#[derive(Clone)]
-pub struct RepositoryHandle {
+pub struct Repository {
+    commit_message_buffer: Option<Entity<Buffer>>,
     git_state: WeakEntity<GitState>,
     pub worktree_id: WorktreeId,
     pub repository_entry: RepositoryEntry,
@@ -44,25 +49,10 @@ pub enum GitRepo {
     },
 }
 
-impl PartialEq<Self> for RepositoryHandle {
-    fn eq(&self, other: &Self) -> bool {
-        self.worktree_id == other.worktree_id
-            && self.repository_entry.work_directory_id()
-                == other.repository_entry.work_directory_id()
-    }
-}
-
-impl Eq for RepositoryHandle {}
-
-impl PartialEq<RepositoryEntry> for RepositoryHandle {
-    fn eq(&self, other: &RepositoryEntry) -> bool {
-        self.repository_entry.work_directory_id() == other.work_directory_id()
-    }
-}
-
 enum Message {
     Commit {
         git_repo: GitRepo,
+        message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
     },
     Stage(GitRepo, Vec<RepoPath>),
@@ -97,7 +87,7 @@ impl GitState {
         }
     }
 
-    pub fn active_repository(&self) -> Option<RepositoryHandle> {
+    pub fn active_repository(&self) -> Option<Entity<Repository>> {
         self.active_index
             .map(|index| self.repositories[index].clone())
     }
@@ -118,7 +108,7 @@ impl GitState {
 
         worktree_store.update(cx, |worktree_store, cx| {
             for worktree in worktree_store.worktrees() {
-                worktree.update(cx, |worktree, _| {
+                worktree.update(cx, |worktree, cx| {
                     let snapshot = worktree.snapshot();
                     for repo in snapshot.repositories().iter() {
                         let git_repo = worktree
@@ -139,27 +129,34 @@ impl GitState {
                         let Some(git_repo) = git_repo else {
                             continue;
                         };
-                        let existing = self
-                            .repositories
-                            .iter()
-                            .enumerate()
-                            .find(|(_, existing_handle)| existing_handle == &repo);
+                        let worktree_id = worktree.id();
+                        let existing =
+                            self.repositories
+                                .iter()
+                                .enumerate()
+                                .find(|(_, existing_handle)| {
+                                    existing_handle.read(cx).id()
+                                        == (worktree_id, repo.work_directory_id())
+                                });
                         let handle = if let Some((index, handle)) = existing {
                             if self.active_index == Some(index) {
                                 new_active_index = Some(new_repositories.len());
                             }
                             // Update the statuses but keep everything else.
-                            let mut existing_handle = handle.clone();
-                            existing_handle.repository_entry = repo.clone();
+                            let existing_handle = handle.clone();
+                            existing_handle.update(cx, |existing_handle, _| {
+                                existing_handle.repository_entry = repo.clone();
+                            });
                             existing_handle
                         } else {
-                            RepositoryHandle {
+                            cx.new(|_| Repository {
                                 git_state: this.clone(),
-                                worktree_id: worktree.id(),
+                                worktree_id,
                                 repository_entry: repo.clone(),
                                 git_repo,
                                 update_sender: self.update_sender.clone(),
-                            }
+                                commit_message_buffer: None,
+                            })
                         };
                         new_repositories.push(handle);
                     }
@@ -184,7 +181,7 @@ impl GitState {
         }
     }
 
-    pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
+    pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
         self.repositories.clone()
     }
 
@@ -260,10 +257,12 @@ impl GitState {
             }
             Message::Commit {
                 git_repo,
+                message,
                 name_and_email,
             } => {
                 match git_repo {
                     GitRepo::Local(repo) => repo.commit(
+                        message.as_ref(),
                         name_and_email
                             .as_ref()
                             .map(|(name, email)| (name.as_ref(), email.as_ref())),
@@ -280,6 +279,7 @@ impl GitState {
                                 project_id: project_id.0,
                                 worktree_id: worktree_id.to_proto(),
                                 work_directory_id: work_directory_id.to_proto(),
+                                message: String::from(message),
                                 name: name.map(String::from),
                                 email: email.map(String::from),
                             })
@@ -293,7 +293,11 @@ impl GitState {
     }
 }
 
-impl RepositoryHandle {
+impl Repository {
+    fn id(&self) -> (WorktreeId, ProjectEntryId) {
+        (self.worktree_id, self.repository_entry.work_directory_id())
+    }
+
     pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
         maybe!({
             let path = self.repo_path_to_project_path(&"".into())?;
@@ -318,7 +322,7 @@ impl RepositoryHandle {
                 .repositories
                 .iter()
                 .enumerate()
-                .find(|(_, handle)| handle == &self)
+                .find(|(_, handle)| handle.read(cx).id() == self.id())
             else {
                 return;
             };
@@ -343,47 +347,121 @@ impl RepositoryHandle {
         self.repository_entry.relativize(&path.path).log_err()
     }
 
-    pub async fn stage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
-        if entries.is_empty() {
-            return Ok(());
+    pub fn open_commit_buffer(
+        &mut self,
+        languages: Option<Arc<LanguageRegistry>>,
+        buffer_store: Entity<BufferStore>,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<Entity<Buffer>>> {
+        if let Some(buffer) = self.commit_message_buffer.clone() {
+            return Task::ready(Ok(buffer));
         }
+
+        if let GitRepo::Remote {
+            project_id,
+            client,
+            worktree_id,
+            work_directory_id,
+        } = self.git_repo.clone()
+        {
+            let client = client.clone();
+            cx.spawn(|repository, mut cx| async move {
+                let request = client.request(proto::OpenCommitMessageBuffer {
+                    project_id: project_id.0,
+                    worktree_id: worktree_id.to_proto(),
+                    work_directory_id: work_directory_id.to_proto(),
+                });
+                let response = request.await.context("requesting to open commit buffer")?;
+                let buffer_id = BufferId::new(response.buffer_id)?;
+                let buffer = buffer_store
+                    .update(&mut cx, |buffer_store, cx| {
+                        buffer_store.wait_for_remote_buffer(buffer_id, cx)
+                    })?
+                    .await?;
+                if let Some(language_registry) = languages {
+                    let git_commit_language =
+                        language_registry.language_for_name("Git Commit").await?;
+                    buffer.update(&mut cx, |buffer, cx| {
+                        buffer.set_language(Some(git_commit_language), cx);
+                    })?;
+                }
+                repository.update(&mut cx, |repository, _| {
+                    repository.commit_message_buffer = Some(buffer.clone());
+                })?;
+                Ok(buffer)
+            })
+        } else {
+            self.open_local_commit_buffer(languages, buffer_store, cx)
+        }
+    }
+
+    fn open_local_commit_buffer(
+        &mut self,
+        language_registry: Option<Arc<LanguageRegistry>>,
+        buffer_store: Entity<BufferStore>,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<Entity<Buffer>>> {
+        cx.spawn(|repository, mut cx| async move {
+            let buffer = buffer_store
+                .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
+                .await?;
+
+            if let Some(language_registry) = language_registry {
+                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_language(Some(git_commit_language), cx);
+                })?;
+            }
+
+            repository.update(&mut cx, |repository, _| {
+                repository.commit_message_buffer = Some(buffer.clone());
+            })?;
+            Ok(buffer)
+        })
+    }
+
+    pub fn stage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
         let (result_tx, result_rx) = futures::channel::oneshot::channel();
+        if entries.is_empty() {
+            result_tx.send(Ok(())).ok();
+            return result_rx;
+        }
         self.update_sender
             .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx))
-            .map_err(|_| anyhow!("Failed to submit stage operation"))?;
-
-        result_rx.await?
+            .ok();
+        result_rx
     }
 
-    pub async fn unstage_entries(&self, entries: Vec<RepoPath>) -> anyhow::Result<()> {
+    pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
+        let (result_tx, result_rx) = futures::channel::oneshot::channel();
         if entries.is_empty() {
-            return Ok(());
+            result_tx.send(Ok(())).ok();
+            return result_rx;
         }
-        let (result_tx, result_rx) = futures::channel::oneshot::channel();
         self.update_sender
             .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx))
-            .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
-        result_rx.await?
+            .ok();
+        result_rx
     }
 
-    pub async fn stage_all(&self) -> anyhow::Result<()> {
+    pub fn stage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
         let to_stage = self
             .repository_entry
             .status()
             .filter(|entry| !entry.status.is_staged().unwrap_or(false))
             .map(|entry| entry.repo_path.clone())
             .collect();
-        self.stage_entries(to_stage).await
+        self.stage_entries(to_stage)
     }
 
-    pub async fn unstage_all(&self) -> anyhow::Result<()> {
+    pub fn unstage_all(&self) -> oneshot::Receiver<anyhow::Result<()>> {
         let to_unstage = self
             .repository_entry
             .status()
             .filter(|entry| entry.status.is_staged().unwrap_or(true))
             .map(|entry| entry.repo_path.clone())
             .collect();
-        self.unstage_entries(to_unstage).await
+        self.unstage_entries(to_unstage)
     }
 
     /// Get a count of all entries in the active repository, including
@@ -404,18 +482,22 @@ impl RepositoryHandle {
         return self.have_changes() && (commit_all || self.have_staged_changes());
     }
 
-    pub async fn commit(
+    pub fn commit(
         &self,
+        message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
-    ) -> anyhow::Result<()> {
+    ) -> oneshot::Receiver<anyhow::Result<()>> {
         let (result_tx, result_rx) = futures::channel::oneshot::channel();
-        self.update_sender.unbounded_send((
-            Message::Commit {
-                git_repo: self.git_repo.clone(),
-                name_and_email,
-            },
-            result_tx,
-        ))?;
-        result_rx.await?
+        self.update_sender
+            .unbounded_send((
+                Message::Commit {
+                    git_repo: self.git_repo.clone(),
+                    message,
+                    name_and_email,
+                },
+                result_tx,
+            ))
+            .ok();
+        result_rx
     }
 }

crates/project/src/project.rs 🔗

@@ -22,7 +22,7 @@ mod project_tests;
 mod direnv;
 mod environment;
 pub use environment::EnvironmentErrorMessage;
-use git::RepositoryHandle;
+use git::Repository;
 pub mod search_history;
 mod yarn;
 
@@ -48,7 +48,6 @@ use ::git::{
     blame::Blame,
     repository::{Branch, GitRepository, RepoPath},
     status::FileStatus,
-    COMMIT_MESSAGE,
 };
 use gpui::{
     AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
@@ -1998,12 +1997,15 @@ impl Project {
                 project_id,
                 id: id.into(),
             });
-            cx.spawn(move |this, mut cx| async move {
+            cx.spawn(move |project, mut cx| async move {
                 let buffer_id = BufferId::new(request.await?.buffer_id)?;
-                this.update(&mut cx, |this, cx| {
-                    this.wait_for_remote_buffer(buffer_id, cx)
-                })?
-                .await
+                project
+                    .update(&mut cx, |project, cx| {
+                        project.buffer_store.update(cx, |buffer_store, cx| {
+                            buffer_store.wait_for_remote_buffer(buffer_id, cx)
+                        })
+                    })?
+                    .await
             })
         } else {
             Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
@@ -2846,16 +2848,21 @@ impl Project {
 
         let proto_client = ssh_client.read(cx).proto_client();
 
-        cx.spawn(|this, mut cx| async move {
+        cx.spawn(|project, mut cx| async move {
             let buffer = proto_client
                 .request(proto::OpenServerSettings {
                     project_id: SSH_PROJECT_ID,
                 })
                 .await?;
 
-            let buffer = this
-                .update(&mut cx, |this, cx| {
-                    anyhow::Ok(this.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx))
+            let buffer = project
+                .update(&mut cx, |project, cx| {
+                    project.buffer_store.update(cx, |buffer_store, cx| {
+                        anyhow::Ok(
+                            buffer_store
+                                .wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx),
+                        )
+                    })
                 })??
                 .await;
 
@@ -3186,13 +3193,15 @@ impl Project {
         });
         let guard = self.retain_remotely_created_models(cx);
 
-        cx.spawn(move |this, mut cx| async move {
+        cx.spawn(move |project, mut cx| async move {
             let response = request.await?;
             for buffer_id in response.buffer_ids {
                 let buffer_id = BufferId::new(buffer_id)?;
-                let buffer = this
-                    .update(&mut cx, |this, cx| {
-                        this.wait_for_remote_buffer(buffer_id, cx)
+                let buffer = project
+                    .update(&mut cx, |project, cx| {
+                        project.buffer_store.update(cx, |buffer_store, cx| {
+                            buffer_store.wait_for_remote_buffer(buffer_id, cx)
+                        })
                     })?
                     .await?;
                 let _ = tx.send(buffer).await;
@@ -3998,7 +4007,11 @@ impl Project {
             .map(RepoPath::new)
             .collect();
 
-        repository_handle.stage_entries(entries).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.stage_entries(entries)
+            })?
+            .await??;
         Ok(proto::Ack {})
     }
 
@@ -4020,7 +4033,11 @@ impl Project {
             .map(RepoPath::new)
             .collect();
 
-        repository_handle.unstage_entries(entries).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.unstage_entries(entries)
+            })?
+            .await??;
         Ok(proto::Ack {})
     }
 
@@ -4034,9 +4051,14 @@ impl Project {
         let repository_handle =
             Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
+        let message = SharedString::from(envelope.payload.message);
         let name = envelope.payload.name.map(SharedString::from);
         let email = envelope.payload.email.map(SharedString::from);
-        repository_handle.commit(name.zip(email)).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.commit(message, name.zip(email))
+            })?
+            .await??;
         Ok(proto::Ack {})
     }
 
@@ -4049,55 +4071,12 @@ impl Project {
         let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
         let repository_handle =
             Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
-        let git_repository = match &repository_handle.git_repo {
-            git::GitRepo::Local(git_repository) => git_repository.clone(),
-            git::GitRepo::Remote { .. } => {
-                anyhow::bail!("Cannot handle open commit message buffer for remote git repo")
-            }
-        };
-        let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE);
-        let fs = this.update(&mut cx, |project, _| project.fs().clone())?;
-        fs.create_file(
-            &commit_message_file,
-            CreateOptions {
-                overwrite: false,
-                ignore_if_exists: true,
-            },
-        )
-        .await
-        .with_context(|| format!("creating commit message file {commit_message_file:?}"))?;
-
-        let (worktree, relative_path) = this
-            .update(&mut cx, |headless_project, cx| {
-                headless_project
-                    .worktree_store
-                    .update(cx, |worktree_store, cx| {
-                        worktree_store.find_or_create_worktree(&commit_message_file, false, cx)
-                    })
-            })?
-            .await
-            .with_context(|| {
-                format!("deriving worktree for commit message file {commit_message_file:?}")
-            })?;
-
-        let buffer = this
-            .update(&mut cx, |headless_project, cx| {
-                headless_project
-                    .buffer_store
-                    .update(cx, |buffer_store, cx| {
-                        buffer_store.open_buffer(
-                            ProjectPath {
-                                worktree_id: worktree.read(cx).id(),
-                                path: Arc::from(relative_path),
-                            },
-                            cx,
-                        )
-                    })
-            })
-            .with_context(|| {
-                format!("opening buffer for commit message file {commit_message_file:?}")
+        let buffer = repository_handle
+            .update(&mut cx, |repository_handle, cx| {
+                repository_handle.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
             })?
             .await?;
+
         let peer_id = envelope.original_sender_id()?;
         Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
     }
@@ -4107,7 +4086,7 @@ impl Project {
         worktree_id: WorktreeId,
         work_directory_id: ProjectEntryId,
         cx: &mut AsyncApp,
-    ) -> Result<RepositoryHandle> {
+    ) -> Result<Entity<Repository>> {
         this.update(cx, |project, cx| {
             let repository_handle = project
                 .git_state()
@@ -4115,6 +4094,7 @@ impl Project {
                 .all_repositories()
                 .into_iter()
                 .find(|repository_handle| {
+                    let repository_handle = repository_handle.read(cx);
                     repository_handle.worktree_id == worktree_id
                         && repository_handle.repository_entry.work_directory_id()
                             == work_directory_id
@@ -4160,16 +4140,6 @@ impl Project {
         buffer.read(cx).remote_id()
     }
 
-    pub fn wait_for_remote_buffer(
-        &mut self,
-        id: BufferId,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<Buffer>>> {
-        self.buffer_store.update(cx, |buffer_store, cx| {
-            buffer_store.wait_for_remote_buffer(id, cx)
-        })
-    }
-
     fn synchronize_remote_buffers(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
         let project_id = match self.client_state {
             ProjectClientState::Remote {
@@ -4329,11 +4299,11 @@ impl Project {
         &self.git_state
     }
 
-    pub fn active_repository(&self, cx: &App) -> Option<RepositoryHandle> {
+    pub fn active_repository(&self, cx: &App) -> Option<Entity<Repository>> {
         self.git_state.read(cx).active_repository()
     }
 
-    pub fn all_repositories(&self, cx: &App) -> Vec<RepositoryHandle> {
+    pub fn all_repositories(&self, cx: &App) -> Vec<Entity<Repository>> {
         self.git_state.read(cx).all_repositories()
     }
 }

crates/proto/proto/zed.proto 🔗

@@ -2693,6 +2693,7 @@ message Commit {
     uint64 work_directory_id = 3;
     optional string name = 4;
     optional string email = 5;
+    string message = 6;
 }
 
 message OpenCommitMessageBuffer {

crates/remote_server/src/headless_project.rs 🔗

@@ -1,15 +1,15 @@
 use anyhow::{anyhow, Context as _, Result};
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
-use fs::{CreateOptions, Fs};
-use git::{repository::RepoPath, COMMIT_MESSAGE};
+use fs::Fs;
+use git::repository::RepoPath;
 use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
 use http_client::HttpClient;
 use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
 use node_runtime::NodeRuntime;
 use project::{
     buffer_store::{BufferStore, BufferStoreEvent},
-    git::{GitRepo, GitState, RepositoryHandle},
+    git::{GitState, Repository},
     project_settings::SettingsObserver,
     search::SearchQuery,
     task_store::TaskStore,
@@ -635,7 +635,11 @@ impl HeadlessProject {
             .map(RepoPath::new)
             .collect();
 
-        repository_handle.stage_entries(entries).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.stage_entries(entries)
+            })?
+            .await??;
         Ok(proto::Ack {})
     }
 
@@ -657,7 +661,11 @@ impl HeadlessProject {
             .map(RepoPath::new)
             .collect();
 
-        repository_handle.unstage_entries(entries).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.unstage_entries(entries)
+            })?
+            .await??;
 
         Ok(proto::Ack {})
     }
@@ -672,10 +680,15 @@ impl HeadlessProject {
         let repository_handle =
             Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
+        let message = SharedString::from(envelope.payload.message);
         let name = envelope.payload.name.map(SharedString::from);
         let email = envelope.payload.email.map(SharedString::from);
 
-        repository_handle.commit(name.zip(email)).await?;
+        repository_handle
+            .update(&mut cx, |repository_handle, _| {
+                repository_handle.commit(message, name.zip(email))
+            })?
+            .await??;
         Ok(proto::Ack {})
     }
 
@@ -686,55 +699,11 @@ impl HeadlessProject {
     ) -> Result<proto::OpenBufferResponse> {
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
         let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
-        let repository_handle =
+        let repository =
             Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
-        let git_repository = match &repository_handle.git_repo {
-            GitRepo::Local(git_repository) => git_repository.clone(),
-            GitRepo::Remote { .. } => {
-                anyhow::bail!("Cannot handle open commit message buffer for remote git repo")
-            }
-        };
-        let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE);
-        let fs = this.update(&mut cx, |headless_project, _| headless_project.fs.clone())?;
-        fs.create_file(
-            &commit_message_file,
-            CreateOptions {
-                overwrite: false,
-                ignore_if_exists: true,
-            },
-        )
-        .await
-        .with_context(|| format!("creating commit message file {commit_message_file:?}"))?;
-
-        let (worktree, relative_path) = this
-            .update(&mut cx, |headless_project, cx| {
-                headless_project
-                    .worktree_store
-                    .update(cx, |worktree_store, cx| {
-                        worktree_store.find_or_create_worktree(&commit_message_file, false, cx)
-                    })
-            })?
-            .await
-            .with_context(|| {
-                format!("deriving worktree for commit message file {commit_message_file:?}")
-            })?;
-
-        let buffer = this
-            .update(&mut cx, |headless_project, cx| {
-                headless_project
-                    .buffer_store
-                    .update(cx, |buffer_store, cx| {
-                        buffer_store.open_buffer(
-                            ProjectPath {
-                                worktree_id: worktree.read(cx).id(),
-                                path: Arc::from(relative_path),
-                            },
-                            cx,
-                        )
-                    })
-            })
-            .with_context(|| {
-                format!("opening buffer for commit message file {commit_message_file:?}")
+        let buffer = repository
+            .update(&mut cx, |repository, cx| {
+                repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
             })?
             .await?;
 
@@ -759,7 +728,7 @@ impl HeadlessProject {
         worktree_id: WorktreeId,
         work_directory_id: ProjectEntryId,
         cx: &mut AsyncApp,
-    ) -> Result<RepositoryHandle> {
+    ) -> Result<Entity<Repository>> {
         this.update(cx, |project, cx| {
             let repository_handle = project
                 .git_state
@@ -767,8 +736,11 @@ impl HeadlessProject {
                 .all_repositories()
                 .into_iter()
                 .find(|repository_handle| {
-                    repository_handle.worktree_id == worktree_id
-                        && repository_handle.repository_entry.work_directory_id()
+                    repository_handle.read(cx).worktree_id == worktree_id
+                        && repository_handle
+                            .read(cx)
+                            .repository_entry
+                            .work_directory_id()
                             == work_directory_id
                 })
                 .context("missing repository handle")?;

crates/worktree/src/worktree.rs 🔗

@@ -199,7 +199,7 @@ pub struct RepositoryEntry {
     ///     - my_sub_folder_1/project_root/changed_file_1
     ///     - my_sub_folder_2/changed_file_2
     pub(crate) statuses_by_path: SumTree<StatusEntry>,
-    pub work_directory_id: ProjectEntryId,
+    work_directory_id: ProjectEntryId,
     pub work_directory: WorkDirectory,
     pub(crate) branch: Option<Arc<str>>,
 }