Enable collaborating editing of the commit message input inside the git panel (#24130)

Kirill Bulatov and Cole Miller created

https://github.com/user-attachments/assets/200b88b8-249a-4841-97cd-fda8365efd00

Now all users in the collab/ssh session can edit the commit input
collaboratively, observing each others' changes live.

A real `.git/COMMIT_EDITMSG` file is opened, which automatically enables
its syntax highlight, but its original context is never used or saved on
disk β€” this way we avoid stale commit messages from previous commits
that git places there.

A caveat: previous version put some effort into preserving unfinished
commit messages on repo swtiches, but this version would not do that
β€”Β instead, it will be blank on startup, and use whatever
`.git/COMMIT_EDITMSG` contents on repo switch

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

Cargo.lock                                   |   2 
crates/collab/src/rpc.rs                     |   1 
crates/editor/src/editor.rs                  |   4 
crates/git/src/repository.rs                 |  18 
crates/git_ui/Cargo.toml                     |   2 
crates/git_ui/src/git_panel.rs               | 236 ++++++++++++-
crates/project/src/git.rs                    | 372 ++++++++-------------
crates/project/src/project.rs                | 147 +++++---
crates/proto/proto/zed.proto                 |  14 
crates/proto/src/proto.rs                    |   3 
crates/remote_server/src/headless_project.rs | 158 ++++++--
11 files changed, 594 insertions(+), 363 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -5264,9 +5264,11 @@ dependencies = [
  "futures 0.3.31",
  "git",
  "gpui",
+ "language",
  "menu",
  "picker",
  "project",
+ "rpc",
  "schemars",
  "serde",
  "serde_derive",

crates/collab/src/rpc.rs πŸ”—

@@ -394,6 +394,7 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::Stage>)
             .add_request_handler(forward_mutating_project_request::<proto::Unstage>)
             .add_request_handler(forward_mutating_project_request::<proto::Commit>)
+            .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
             .add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
             .add_message_handler(update_context)
             .add_request_handler({

crates/editor/src/editor.rs πŸ”—

@@ -12060,6 +12060,10 @@ impl Editor {
         self.buffer.read(cx).read(cx).text()
     }
 
+    pub fn is_empty(&self, cx: &App) -> bool {
+        self.buffer.read(cx).read(cx).is_empty()
+    }
+
     pub fn text_option(&self, cx: &App) -> Option<String> {
         let text = self.text(cx);
         let text = text.trim();

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;
@@ -62,7 +62,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, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
+    fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()>;
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -283,14 +283,22 @@ impl GitRepository for RealGitRepository {
         Ok(())
     }
 
-    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(&self, 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 mut args = vec!["commit", "--quiet", "-m", message];
+        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 author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
         if let Some(author) = author.as_deref() {
             args.push("--author");
@@ -450,7 +458,7 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 
-    fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
+    fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
         unimplemented!()
     }
 }

crates/git_ui/Cargo.toml πŸ”—

@@ -20,8 +20,10 @@ editor.workspace = true
 futures.workspace = true
 git.workspace = true
 gpui.workspace = true
+language.workspace = true
 menu.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 πŸ”—

@@ -3,20 +3,24 @@ use crate::repository_selector::RepositorySelectorPopoverMenu;
 use crate::{
     git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
 };
-use anyhow::Result;
+use anyhow::{Context as _, Result};
 use db::kvp::KEY_VALUE_STORE;
 use editor::actions::MoveToEnd;
 use editor::scroll::ScrollbarAutoHide;
 use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
 use futures::channel::mpsc;
-use futures::StreamExt as _;
+use futures::{SinkExt, StreamExt as _};
 use git::repository::RepoPath;
 use git::status::FileStatus;
-use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
+use git::{
+    CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll, COMMIT_MESSAGE,
+};
 use gpui::*;
+use language::{Buffer, BufferId};
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use project::git::RepositoryHandle;
-use project::{Fs, Project, ProjectPath};
+use project::git::{GitRepo, RepositoryHandle};
+use project::{CreateOptions, Fs, Project, ProjectPath};
+use rpc::proto;
 use serde::{Deserialize, Serialize};
 use settings::Settings as _;
 use std::{collections::HashSet, ops::Range, path::PathBuf, sync::Arc, time::Duration, usize};
@@ -30,7 +34,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
 use workspace::Toast;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
+    Item, Workspace,
 };
 
 actions!(
@@ -101,10 +105,69 @@ pub struct GitPanel {
     all_staged: Option<bool>,
     width: Option<Pixels>,
     err_sender: mpsc::Sender<anyhow::Error>,
+    commit_task: Task<()>,
+    commit_pending: 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(
-    active_repository: Option<&RepositoryHandle>,
+    commit_message_buffer: Option<Entity<Buffer>>,
     window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) -> Editor {
@@ -121,8 +184,8 @@ fn commit_message_editor(
     };
     text_style.refine(&refinement);
 
-    let mut commit_editor = if let Some(active_repository) = active_repository.as_ref() {
-        let buffer = cx.new(|cx| MultiBuffer::singleton(active_repository.commit_message(), cx));
+    let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
+        let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
         Editor::new(
             EditorMode::AutoHeight { max_lines: 10 },
             buffer,
@@ -148,12 +211,31 @@ impl GitPanel {
         workspace: WeakEntity<Workspace>,
         cx: AsyncWindowContext,
     ) -> Task<Result<Entity<Self>>> {
-        cx.spawn(|mut cx| async move { workspace.update_in(&mut cx, Self::new) })
+        cx.spawn(|mut cx| async move {
+            let commit_message_buffer = workspace.update(&mut cx, |workspace, cx| {
+                let project = workspace.project();
+                let active_repository = project.read(cx).active_repository(cx);
+                active_repository
+                    .map(|active_repository| commit_message_buffer(project, &active_repository, cx))
+            })?;
+            let commit_message_buffer = match commit_message_buffer {
+                Some(commit_message_buffer) => Some(
+                    commit_message_buffer
+                        .await
+                        .context("opening commit buffer")?,
+                ),
+                None => None,
+            };
+            workspace.update_in(&mut cx, |workspace, window, cx| {
+                Self::new(workspace, window, commit_message_buffer, cx)
+            })
+        })
     }
 
     pub fn new(
         workspace: &mut Workspace,
         window: &mut Window,
+        commit_message_buffer: Option<Entity<Buffer>>,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
         let fs = workspace.app_state().fs.clone();
@@ -172,7 +254,10 @@ impl GitPanel {
             .detach();
 
             let commit_editor =
-                cx.new(|cx| commit_message_editor(active_repository.as_ref(), window, cx));
+                cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+            commit_editor.update(cx, |editor, cx| {
+                editor.clear(window, cx);
+            });
 
             let scroll_handle = UniformListScrollHandle::new();
 
@@ -207,6 +292,8 @@ impl GitPanel {
                 show_scrollbar: false,
                 hide_scrollbar_task: None,
                 update_visible_entries_task: Task::ready(()),
+                commit_task: Task::ready(()),
+                commit_pending: false,
                 active_repository,
                 scroll_handle,
                 fs,
@@ -586,16 +673,49 @@ impl GitPanel {
         &mut self,
         _: &git::CommitChanges,
         name_and_email: Option<(SharedString, SharedString)>,
-        _window: &mut Window,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(active_repository) = self.active_repository.as_ref() else {
+        let Some(active_repository) = self.active_repository.clone() else {
             return;
         };
-        if !active_repository.can_commit(false, cx) {
+        if !active_repository.can_commit(false) {
+            return;
+        }
+        if self.commit_editor.read(cx).is_empty(cx) {
             return;
         }
-        active_repository.commit(name_and_email, self.err_sender.clone(), cx);
+        self.commit_pending = true;
+        let save_task = self.commit_editor.update(cx, |editor, cx| {
+            editor.save(false, self.project.clone(), window, cx)
+        });
+        let mut err_sender = self.err_sender.clone();
+        let commit_editor = self.commit_editor.clone();
+        self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
+            match save_task.await {
+                Ok(()) => {
+                    if let Some(Ok(())) = cx
+                        .update(|_, cx| {
+                            active_repository.commit(name_and_email, err_sender.clone(), cx)
+                        })
+                        .ok()
+                    {
+                        cx.update(|window, cx| {
+                            commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
+                        })
+                        .ok();
+                    }
+                }
+                Err(e) => {
+                    err_sender.send(e).await.ok();
+                }
+            }
+            git_panel
+                .update(&mut cx, |git_panel, _| {
+                    git_panel.commit_pending = false;
+                })
+                .ok();
+        });
     }
 
     /// Commit all changes, regardless of whether they are staged or not
@@ -603,16 +723,49 @@ impl GitPanel {
         &mut self,
         _: &git::CommitAllChanges,
         name_and_email: Option<(SharedString, SharedString)>,
-        _window: &mut Window,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(active_repository) = self.active_repository.as_ref() else {
+        let Some(active_repository) = self.active_repository.clone() else {
             return;
         };
-        if !active_repository.can_commit(true, cx) {
+        if !active_repository.can_commit(true) {
+            return;
+        }
+        if self.commit_editor.read(cx).is_empty(cx) {
             return;
         }
-        active_repository.commit_all(name_and_email, self.err_sender.clone(), cx);
+        self.commit_pending = true;
+        let save_task = self.commit_editor.update(cx, |editor, cx| {
+            editor.save(false, self.project.clone(), window, cx)
+        });
+        let mut err_sender = self.err_sender.clone();
+        let commit_editor = self.commit_editor.clone();
+        self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
+            match save_task.await {
+                Ok(()) => {
+                    if let Some(Ok(())) = cx
+                        .update(|_, cx| {
+                            active_repository.commit_all(name_and_email, err_sender.clone(), cx)
+                        })
+                        .ok()
+                    {
+                        cx.update(|window, cx| {
+                            commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
+                        })
+                        .ok();
+                    }
+                }
+                Err(e) => {
+                    err_sender.send(e).await.ok();
+                }
+            }
+            git_panel
+                .update(&mut cx, |git_panel, _| {
+                    git_panel.commit_pending = false;
+                })
+                .ok();
+        });
     }
 
     fn fill_co_authors(&mut self, _: &FillCoAuthors, window: &mut Window, cx: &mut Context<Self>) {
@@ -714,17 +867,40 @@ impl GitPanel {
     }
 
     fn schedule_update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let project = self.project.clone();
         let handle = cx.entity().downgrade();
         self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
             cx.background_executor().timer(UPDATE_DEBOUNCE).await;
-            if let Some(this) = handle.upgrade() {
-                this.update_in(&mut cx, |this, window, cx| {
-                    this.update_visible_entries(cx);
-                    let active_repository = this.active_repository.as_ref();
-                    this.commit_editor =
-                        cx.new(|cx| commit_message_editor(active_repository, window, cx));
-                })
-                .ok();
+            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);
+                        git_panel.commit_editor =
+                            cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+                    })
+                    .ok();
             }
         });
     }
@@ -963,14 +1139,15 @@ impl GitPanel {
         cx: &Context<Self>,
     ) -> impl IntoElement {
         let editor = self.commit_editor.clone();
+        let can_commit = can_commit && !editor.read(cx).is_empty(cx);
         let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
         let (can_commit, can_commit_all) =
             self.active_repository
                 .as_ref()
                 .map_or((false, false), |active_repository| {
                     (
-                        can_commit && active_repository.can_commit(false, cx),
-                        can_commit && active_repository.can_commit(true, cx),
+                        can_commit && active_repository.can_commit(false),
+                        can_commit && active_repository.can_commit(true),
                     )
                 });
 
@@ -1306,6 +1483,7 @@ impl Render for GitPanel {
             }
             None => (has_write_access, None),
         };
+        let can_commit = !self.commit_pending && can_commit;
 
         let has_co_authors = can_commit
             && has_write_access

crates/project/src/git.rs πŸ”—

@@ -8,14 +8,10 @@ use git::{
     repository::{GitRepository, RepoPath},
     status::{GitSummary, TrackedSummary},
 };
-use gpui::{
-    App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
-};
-use language::{Buffer, LanguageRegistry};
+use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
 use rpc::{proto, AnyProtoClient};
 use settings::WorktreeId;
 use std::sync::Arc;
-use text::Rope;
 use util::maybe;
 use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
 
@@ -25,7 +21,6 @@ pub struct GitState {
     repositories: Vec<RepositoryHandle>,
     active_index: Option<usize>,
     update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
-    languages: Arc<LanguageRegistry>,
     _subscription: Subscription,
 }
 
@@ -34,13 +29,12 @@ pub struct RepositoryHandle {
     git_state: WeakEntity<GitState>,
     pub worktree_id: WorktreeId,
     pub repository_entry: RepositoryEntry,
-    git_repo: Option<GitRepo>,
-    commit_message: Entity<Buffer>,
+    pub git_repo: GitRepo,
     update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
 }
 
 #[derive(Clone)]
-enum GitRepo {
+pub enum GitRepo {
     Local(Arc<dyn GitRepository>),
     Remote {
         project_id: ProjectId,
@@ -70,12 +64,10 @@ enum Message {
     StageAndCommit {
         git_repo: GitRepo,
         paths: Vec<RepoPath>,
-        message: Rope,
         name_and_email: Option<(SharedString, SharedString)>,
     },
     Commit {
         git_repo: GitRepo,
-        message: Rope,
         name_and_email: Option<(SharedString, SharedString)>,
     },
     Stage(GitRepo, Vec<RepoPath>),
@@ -91,7 +83,6 @@ impl EventEmitter<Event> for GitState {}
 impl GitState {
     pub fn new(
         worktree_store: &Entity<WorktreeStore>,
-        languages: Arc<LanguageRegistry>,
         client: Option<AnyProtoClient>,
         project_id: Option<ProjectId>,
         cx: &mut Context<'_, Self>,
@@ -100,150 +91,140 @@ impl GitState {
             mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
         cx.spawn(|_, cx| async move {
             while let Some((msg, mut err_sender)) = update_receiver.next().await {
-                let result = cx
-                    .background_executor()
-                    .spawn(async move {
-                        match msg {
-                            Message::StageAndCommit {
-                                git_repo,
-                                message,
-                                name_and_email,
-                                paths,
-                            } => {
-                                match git_repo {
-                                    GitRepo::Local(repo) => {
-                                        repo.stage_paths(&paths)?;
-                                        repo.commit(
-                                            &message.to_string(),
-                                            name_and_email.as_ref().map(|(name, email)| {
-                                                (name.as_ref(), email.as_ref())
-                                            }),
-                                        )?;
-                                    }
-                                    GitRepo::Remote {
-                                        project_id,
-                                        client,
-                                        worktree_id,
-                                        work_directory_id,
-                                    } => {
-                                        client
-                                            .request(proto::Stage {
-                                                project_id: project_id.0,
-                                                worktree_id: worktree_id.to_proto(),
-                                                work_directory_id: work_directory_id.to_proto(),
-                                                paths: paths
-                                                    .into_iter()
-                                                    .map(|repo_path| repo_path.to_proto())
-                                                    .collect(),
-                                            })
-                                            .await
-                                            .context("sending stage request")?;
-                                        let (name, email) = name_and_email.unzip();
-                                        client
-                                            .request(proto::Commit {
-                                                project_id: project_id.0,
-                                                worktree_id: worktree_id.to_proto(),
-                                                work_directory_id: work_directory_id.to_proto(),
-                                                message: message.to_string(),
-                                                name: name.map(String::from),
-                                                email: email.map(String::from),
-                                            })
-                                            .await
-                                            .context("sending commit request")?;
+                let result =
+                    cx.background_executor()
+                        .spawn(async move {
+                            match msg {
+                                Message::StageAndCommit {
+                                    git_repo,
+                                    name_and_email,
+                                    paths,
+                                } => {
+                                    match git_repo {
+                                        GitRepo::Local(repo) => {
+                                            repo.stage_paths(&paths)?;
+                                            repo.commit(name_and_email.as_ref().map(
+                                                |(name, email)| (name.as_ref(), email.as_ref()),
+                                            ))?;
+                                        }
+                                        GitRepo::Remote {
+                                            project_id,
+                                            client,
+                                            worktree_id,
+                                            work_directory_id,
+                                        } => {
+                                            client
+                                                .request(proto::Stage {
+                                                    project_id: project_id.0,
+                                                    worktree_id: worktree_id.to_proto(),
+                                                    work_directory_id: work_directory_id.to_proto(),
+                                                    paths: paths
+                                                        .into_iter()
+                                                        .map(|repo_path| repo_path.to_proto())
+                                                        .collect(),
+                                                })
+                                                .await
+                                                .context("sending stage request")?;
+                                            let (name, email) = name_and_email.unzip();
+                                            client
+                                                .request(proto::Commit {
+                                                    project_id: project_id.0,
+                                                    worktree_id: worktree_id.to_proto(),
+                                                    work_directory_id: work_directory_id.to_proto(),
+                                                    name: name.map(String::from),
+                                                    email: email.map(String::from),
+                                                })
+                                                .await
+                                                .context("sending commit request")?;
+                                        }
                                     }
-                                }
 
-                                Ok(())
-                            }
-                            Message::Stage(repo, paths) => {
-                                match repo {
-                                    GitRepo::Local(repo) => repo.stage_paths(&paths)?,
-                                    GitRepo::Remote {
-                                        project_id,
-                                        client,
-                                        worktree_id,
-                                        work_directory_id,
-                                    } => {
-                                        client
-                                            .request(proto::Stage {
-                                                project_id: project_id.0,
-                                                worktree_id: worktree_id.to_proto(),
-                                                work_directory_id: work_directory_id.to_proto(),
-                                                paths: paths
-                                                    .into_iter()
-                                                    .map(|repo_path| repo_path.to_proto())
-                                                    .collect(),
-                                            })
-                                            .await
-                                            .context("sending stage request")?;
+                                    Ok(())
+                                }
+                                Message::Stage(repo, paths) => {
+                                    match repo {
+                                        GitRepo::Local(repo) => repo.stage_paths(&paths)?,
+                                        GitRepo::Remote {
+                                            project_id,
+                                            client,
+                                            worktree_id,
+                                            work_directory_id,
+                                        } => {
+                                            client
+                                                .request(proto::Stage {
+                                                    project_id: project_id.0,
+                                                    worktree_id: worktree_id.to_proto(),
+                                                    work_directory_id: work_directory_id.to_proto(),
+                                                    paths: paths
+                                                        .into_iter()
+                                                        .map(|repo_path| repo_path.to_proto())
+                                                        .collect(),
+                                                })
+                                                .await
+                                                .context("sending stage request")?;
+                                        }
                                     }
+                                    Ok(())
                                 }
-                                Ok(())
-                            }
-                            Message::Unstage(repo, paths) => {
-                                match repo {
-                                    GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
-                                    GitRepo::Remote {
-                                        project_id,
-                                        client,
-                                        worktree_id,
-                                        work_directory_id,
-                                    } => {
-                                        client
-                                            .request(proto::Unstage {
-                                                project_id: project_id.0,
-                                                worktree_id: worktree_id.to_proto(),
-                                                work_directory_id: work_directory_id.to_proto(),
-                                                paths: paths
-                                                    .into_iter()
-                                                    .map(|repo_path| repo_path.to_proto())
-                                                    .collect(),
-                                            })
-                                            .await
-                                            .context("sending unstage request")?;
+                                Message::Unstage(repo, paths) => {
+                                    match repo {
+                                        GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
+                                        GitRepo::Remote {
+                                            project_id,
+                                            client,
+                                            worktree_id,
+                                            work_directory_id,
+                                        } => {
+                                            client
+                                                .request(proto::Unstage {
+                                                    project_id: project_id.0,
+                                                    worktree_id: worktree_id.to_proto(),
+                                                    work_directory_id: work_directory_id.to_proto(),
+                                                    paths: paths
+                                                        .into_iter()
+                                                        .map(|repo_path| repo_path.to_proto())
+                                                        .collect(),
+                                                })
+                                                .await
+                                                .context("sending unstage request")?;
+                                        }
                                     }
+                                    Ok(())
                                 }
-                                Ok(())
-                            }
-                            Message::Commit {
-                                git_repo,
-                                message,
-                                name_and_email,
-                            } => {
-                                match git_repo {
-                                    GitRepo::Local(repo) => repo.commit(
-                                        &message.to_string(),
-                                        name_and_email
-                                            .as_ref()
-                                            .map(|(name, email)| (name.as_ref(), email.as_ref())),
-                                    )?,
-                                    GitRepo::Remote {
-                                        project_id,
-                                        client,
-                                        worktree_id,
-                                        work_directory_id,
-                                    } => {
-                                        let (name, email) = name_and_email.unzip();
-                                        client
-                                            .request(proto::Commit {
-                                                project_id: project_id.0,
-                                                worktree_id: worktree_id.to_proto(),
-                                                work_directory_id: work_directory_id.to_proto(),
-                                                // TODO implement collaborative commit message buffer instead and use it
-                                                // If it works, remove `commit_with_message` method.
-                                                message: message.to_string(),
-                                                name: name.map(String::from),
-                                                email: email.map(String::from),
-                                            })
-                                            .await
-                                            .context("sending commit request")?;
+                                Message::Commit {
+                                    git_repo,
+                                    name_and_email,
+                                } => {
+                                    match git_repo {
+                                        GitRepo::Local(repo) => {
+                                            repo.commit(name_and_email.as_ref().map(
+                                                |(name, email)| (name.as_ref(), email.as_ref()),
+                                            ))?
+                                        }
+                                        GitRepo::Remote {
+                                            project_id,
+                                            client,
+                                            worktree_id,
+                                            work_directory_id,
+                                        } => {
+                                            let (name, email) = name_and_email.unzip();
+                                            client
+                                                .request(proto::Commit {
+                                                    project_id: project_id.0,
+                                                    worktree_id: worktree_id.to_proto(),
+                                                    work_directory_id: work_directory_id.to_proto(),
+                                                    name: name.map(String::from),
+                                                    email: email.map(String::from),
+                                                })
+                                                .await
+                                                .context("sending commit request")?;
+                                        }
                                     }
+                                    Ok(())
                                 }
-                                Ok(())
                             }
-                        }
-                    })
-                    .await;
+                        })
+                        .await;
                 if let Err(e) = result {
                     err_sender.send(e).await.ok();
                 }
@@ -255,7 +236,6 @@ impl GitState {
 
         GitState {
             project_id,
-            languages,
             client,
             repositories: Vec::new(),
             active_index: None,
@@ -285,7 +265,7 @@ impl GitState {
 
         worktree_store.update(cx, |worktree_store, cx| {
             for worktree in worktree_store.worktrees() {
-                worktree.update(cx, |worktree, cx| {
+                worktree.update(cx, |worktree, _| {
                     let snapshot = worktree.snapshot();
                     for repo in snapshot.repositories().iter() {
                         let git_repo = worktree
@@ -303,6 +283,9 @@ impl GitState {
                                     work_directory_id: repo.work_directory_id(),
                                 })
                             });
+                        let Some(git_repo) = git_repo else {
+                            continue;
+                        };
                         let existing = self
                             .repositories
                             .iter()
@@ -317,25 +300,11 @@ impl GitState {
                             existing_handle.repository_entry = repo.clone();
                             existing_handle
                         } else {
-                            let commit_message = cx.new(|cx| Buffer::local("", cx));
-                            cx.spawn({
-                                let commit_message = commit_message.downgrade();
-                                let languages = self.languages.clone();
-                                |_, mut cx| async move {
-                                    let markdown = languages.language_for_name("Markdown").await?;
-                                    commit_message.update(&mut cx, |commit_message, cx| {
-                                        commit_message.set_language(Some(markdown), cx);
-                                    })?;
-                                    anyhow::Ok(())
-                                }
-                            })
-                            .detach_and_log_err(cx);
                             RepositoryHandle {
                                 git_state: this.clone(),
                                 worktree_id: worktree.id(),
                                 repository_entry: repo.clone(),
                                 git_repo,
-                                commit_message,
                                 update_sender: self.update_sender.clone(),
                             }
                         };
@@ -403,10 +372,6 @@ impl RepositoryHandle {
         Some((self.worktree_id, path).into())
     }
 
-    pub fn commit_message(&self) -> Entity<Buffer> {
-        self.commit_message.clone()
-    }
-
     pub fn stage_entries(
         &self,
         entries: Vec<RepoPath>,
@@ -415,11 +380,8 @@ impl RepositoryHandle {
         if entries.is_empty() {
             return Ok(());
         }
-        let Some(git_repo) = self.git_repo.clone() else {
-            return Ok(());
-        };
         self.update_sender
-            .unbounded_send((Message::Stage(git_repo, entries), err_sender))
+            .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
             .map_err(|_| anyhow!("Failed to submit stage operation"))?;
         Ok(())
     }
@@ -432,11 +394,8 @@ impl RepositoryHandle {
         if entries.is_empty() {
             return Ok(());
         }
-        let Some(git_repo) = self.git_repo.clone() else {
-            return Ok(());
-        };
         self.update_sender
-            .unbounded_send((Message::Unstage(git_repo, entries), err_sender))
+            .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
             .map_err(|_| anyhow!("Failed to submit unstage operation"))?;
         Ok(())
     }
@@ -477,14 +436,8 @@ impl RepositoryHandle {
         self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
     }
 
-    pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
-        return self
-            .commit_message
-            .read(cx)
-            .chars()
-            .any(|c| !c.is_ascii_whitespace())
-            && self.have_changes()
-            && (commit_all || self.have_staged_changes());
+    pub fn can_commit(&self, commit_all: bool) -> bool {
+        return self.have_changes() && (commit_all || self.have_staged_changes());
     }
 
     pub fn commit(
@@ -492,15 +445,10 @@ impl RepositoryHandle {
         name_and_email: Option<(SharedString, SharedString)>,
         mut err_sender: mpsc::Sender<anyhow::Error>,
         cx: &mut App,
-    ) {
-        let Some(git_repo) = self.git_repo.clone() else {
-            return;
-        };
-        let message = self.commit_message.read(cx).as_rope().clone();
+    ) -> anyhow::Result<()> {
         let result = self.update_sender.unbounded_send((
             Message::Commit {
-                git_repo,
-                message,
+                git_repo: self.git_repo.clone(),
                 name_and_email,
             },
             err_sender.clone(),
@@ -513,32 +461,10 @@ impl RepositoryHandle {
                     .ok();
             })
             .detach();
-            return;
+            anyhow::bail!("Failed to submit commit operation");
+        } else {
+            Ok(())
         }
-        self.commit_message.update(cx, |commit_message, cx| {
-            commit_message.set_text("", cx);
-        });
-    }
-
-    pub fn commit_with_message(
-        &self,
-        message: String,
-        name_and_email: Option<(SharedString, SharedString)>,
-        err_sender: mpsc::Sender<anyhow::Error>,
-    ) -> anyhow::Result<()> {
-        let Some(git_repo) = self.git_repo.clone() else {
-            return Ok(());
-        };
-        let result = self.update_sender.unbounded_send((
-            Message::Commit {
-                git_repo,
-                message: message.into(),
-                name_and_email,
-            },
-            err_sender,
-        ));
-        anyhow::ensure!(result.is_ok(), "Failed to submit commit operation");
-        Ok(())
     }
 
     pub fn commit_all(
@@ -546,22 +472,17 @@ impl RepositoryHandle {
         name_and_email: Option<(SharedString, SharedString)>,
         mut err_sender: mpsc::Sender<anyhow::Error>,
         cx: &mut App,
-    ) {
-        let Some(git_repo) = self.git_repo.clone() else {
-            return;
-        };
+    ) -> 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();
-        let message = self.commit_message.read(cx).as_rope().clone();
         let result = self.update_sender.unbounded_send((
             Message::StageAndCommit {
-                git_repo,
+                git_repo: self.git_repo.clone(),
                 paths: to_stage,
-                message,
                 name_and_email,
             },
             err_sender.clone(),
@@ -574,10 +495,9 @@ impl RepositoryHandle {
                     .ok();
             })
             .detach();
-            return;
+            anyhow::bail!("Failed to submit commit all operation");
+        } else {
+            Ok(())
         }
-        self.commit_message.update(cx, |commit_message, cx| {
-            commit_message.set_text("", cx);
-        });
     }
 }

crates/project/src/project.rs πŸ”—

@@ -48,6 +48,7 @@ use ::git::{
     blame::Blame,
     repository::{Branch, GitRepository, RepoPath},
     status::FileStatus,
+    COMMIT_MESSAGE,
 };
 use gpui::{
     AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
@@ -609,6 +610,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_stage);
         client.add_model_request_handler(Self::handle_unstage);
         client.add_model_request_handler(Self::handle_commit);
+        client.add_model_request_handler(Self::handle_open_commit_message_buffer);
 
         WorktreeStore::init(&client);
         BufferStore::init(&client);
@@ -699,9 +701,7 @@ impl Project {
                 )
             });
 
-            let git_state = Some(
-                cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx)),
-            );
+            let git_state = Some(cx.new(|cx| GitState::new(&worktree_store, None, None, cx)));
 
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 
@@ -824,7 +824,6 @@ impl Project {
             let git_state = Some(cx.new(|cx| {
                 GitState::new(
                     &worktree_store,
-                    languages.clone(),
                     Some(ssh_proto.clone()),
                     Some(ProjectId(SSH_PROJECT_ID)),
                     cx,
@@ -1030,7 +1029,6 @@ impl Project {
         let git_state = Some(cx.new(|cx| {
             GitState::new(
                 &worktree_store,
-                languages.clone(),
                 Some(client.clone().into()),
                 Some(ProjectId(remote_id)),
                 cx,
@@ -3974,21 +3972,8 @@ impl Project {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
-            let repository_handle = project
-                .git_state()
-                .context("missing git state")?
-                .read(cx)
-                .all_repositories()
-                .into_iter()
-                .find(|repository_handle| {
-                    repository_handle.worktree_id == worktree_id
-                        && repository_handle.repository_entry.work_directory_id()
-                            == work_directory_id
-                })
-                .context("missing repository handle")?;
-            anyhow::Ok(repository_handle)
-        })??;
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
         let entries = envelope
             .payload
@@ -4015,21 +4000,8 @@ impl Project {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
-            let repository_handle = project
-                .git_state()
-                .context("missing git state")?
-                .read(cx)
-                .all_repositories()
-                .into_iter()
-                .find(|repository_handle| {
-                    repository_handle.worktree_id == worktree_id
-                        && repository_handle.repository_entry.work_directory_id()
-                            == work_directory_id
-                })
-                .context("missing repository handle")?;
-            anyhow::Ok(repository_handle)
-        })??;
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
         let entries = envelope
             .payload
@@ -4056,7 +4028,93 @@ impl Project {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+        let name = envelope.payload.name.map(SharedString::from);
+        let email = envelope.payload.email.map(SharedString::from);
+        let (err_sender, mut err_receiver) = mpsc::channel(1);
+        cx.update(|cx| {
+            repository_handle
+                .commit(name.zip(email), err_sender, cx)
+                .context("unstaging entries")
+        })??;
+        if let Some(error) = err_receiver.next().await {
+            Err(error.context("error during unstaging"))
+        } else {
+            Ok(proto::Ack {})
+        }
+    }
+
+    async fn handle_open_commit_message_buffer(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
+        mut cx: AsyncApp,
+    ) -> 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 =
+            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:?}")
+            })?
+            .await?;
+        let peer_id = envelope.original_sender_id()?;
+        Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
+    }
+
+    fn repository_for_request(
+        this: &Entity<Self>,
+        worktree_id: WorktreeId,
+        work_directory_id: ProjectEntryId,
+        cx: &mut AsyncApp,
+    ) -> Result<RepositoryHandle> {
+        this.update(cx, |project, cx| {
             let repository_handle = project
                 .git_state()
                 .context("missing git state")?
@@ -4070,20 +4128,7 @@ impl Project {
                 })
                 .context("missing repository handle")?;
             anyhow::Ok(repository_handle)
-        })??;
-
-        let commit_message = envelope.payload.message;
-        let name = envelope.payload.name.map(SharedString::from);
-        let email = envelope.payload.email.map(SharedString::from);
-        let (err_sender, mut err_receiver) = mpsc::channel(1);
-        repository_handle
-            .commit_with_message(commit_message, name.zip(email), err_sender)
-            .context("unstaging entries")?;
-        if let Some(error) = err_receiver.next().await {
-            Err(error.context("error during unstaging"))
-        } else {
-            Ok(proto::Ack {})
-        }
+        })?
     }
 
     fn respond_to_open_buffer_request(
@@ -4122,7 +4167,7 @@ impl Project {
         buffer.read(cx).remote_id()
     }
 
-    fn wait_for_remote_buffer(
+    pub fn wait_for_remote_buffer(
         &mut self,
         id: BufferId,
         cx: &mut Context<Self>,

crates/proto/proto/zed.proto πŸ”—

@@ -311,7 +311,8 @@ message Envelope {
 
         Stage stage = 293;
         Unstage unstage = 294;
-        Commit commit = 295; // current max
+        Commit commit = 295;
+        OpenCommitMessageBuffer open_commit_message_buffer = 296; // current max
     }
 
     reserved 87 to 88;
@@ -2655,7 +2656,12 @@ message Commit {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
     uint64 work_directory_id = 3;
-    string message = 4;
-    optional string name = 5;
-    optional string email = 6;
+    optional string name = 4;
+    optional string email = 5;
+}
+
+message OpenCommitMessageBuffer {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    uint64 work_directory_id = 3;
 }

crates/proto/src/proto.rs πŸ”—

@@ -249,6 +249,7 @@ messages!(
     (OpenBufferForSymbol, Background),
     (OpenBufferForSymbolResponse, Background),
     (OpenBufferResponse, Background),
+    (OpenCommitMessageBuffer, Background),
     (PerformRename, Background),
     (PerformRenameResponse, Background),
     (Ping, Foreground),
@@ -443,6 +444,7 @@ request_messages!(
     (OpenBufferById, OpenBufferResponse),
     (OpenBufferByPath, OpenBufferResponse),
     (OpenBufferForSymbol, OpenBufferForSymbolResponse),
+    (OpenCommitMessageBuffer, OpenBufferResponse),
     (OpenNewBuffer, OpenBufferResponse),
     (PerformRename, PerformRenameResponse),
     (Ping, Ack),
@@ -554,6 +556,7 @@ entity_messages!(
     OpenBufferById,
     OpenBufferByPath,
     OpenBufferForSymbol,
+    OpenCommitMessageBuffer,
     PerformRename,
     PrepareRename,
     RefreshInlayHints,

crates/remote_server/src/headless_project.rs πŸ”—

@@ -1,16 +1,16 @@
 use anyhow::{anyhow, Context as _, Result};
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
-use fs::Fs;
+use fs::{CreateOptions, Fs};
 use futures::channel::mpsc;
-use git::repository::RepoPath;
+use git::{repository::RepoPath, COMMIT_MESSAGE};
 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::GitState,
+    git::{GitRepo, GitState, RepositoryHandle},
     project_settings::SettingsObserver,
     search::SearchQuery,
     task_store::TaskStore,
@@ -83,8 +83,7 @@ impl HeadlessProject {
             store
         });
 
-        let git_state =
-            cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx));
+        let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx));
 
         let buffer_store = cx.new(|cx| {
             let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
@@ -201,6 +200,7 @@ impl HeadlessProject {
         client.add_model_request_handler(Self::handle_stage);
         client.add_model_request_handler(Self::handle_unstage);
         client.add_model_request_handler(Self::handle_commit);
+        client.add_model_request_handler(Self::handle_open_commit_message_buffer);
 
         client.add_request_handler(
             extensions.clone().downgrade(),
@@ -625,20 +625,8 @@ impl HeadlessProject {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
-            let repository_handle = project
-                .git_state
-                .read(cx)
-                .all_repositories()
-                .into_iter()
-                .find(|repository_handle| {
-                    repository_handle.worktree_id == worktree_id
-                        && repository_handle.repository_entry.work_directory_id()
-                            == work_directory_id
-                })
-                .context("missing repository handle")?;
-            anyhow::Ok(repository_handle)
-        })??;
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
         let entries = envelope
             .payload
@@ -665,20 +653,8 @@ impl HeadlessProject {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
-            let repository_handle = project
-                .git_state
-                .read(cx)
-                .all_repositories()
-                .into_iter()
-                .find(|repository_handle| {
-                    repository_handle.worktree_id == worktree_id
-                        && repository_handle.repository_entry.work_directory_id()
-                            == work_directory_id
-                })
-                .context("missing repository handle")?;
-            anyhow::Ok(repository_handle)
-        })??;
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
 
         let entries = envelope
             .payload
@@ -705,7 +681,106 @@ impl HeadlessProject {
     ) -> Result<proto::Ack> {
         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 = this.update(&mut cx, |project, cx| {
+        let repository_handle =
+            Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+        let name = envelope.payload.name.map(SharedString::from);
+        let email = envelope.payload.email.map(SharedString::from);
+        let (err_sender, mut err_receiver) = mpsc::channel(1);
+        cx.update(|cx| {
+            repository_handle
+                .commit(name.zip(email), err_sender, cx)
+                .context("unstaging entries")
+        })??;
+        if let Some(error) = err_receiver.next().await {
+            Err(error.context("error during unstaging"))
+        } else {
+            Ok(proto::Ack {})
+        }
+    }
+
+    async fn handle_open_commit_message_buffer(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
+        mut cx: AsyncApp,
+    ) -> 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 =
+            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:?}")
+            })?
+            .await?;
+
+        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
+        this.update(&mut cx, |headless_project, cx| {
+            headless_project
+                .buffer_store
+                .update(cx, |buffer_store, cx| {
+                    buffer_store
+                        .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
+                        .detach_and_log_err(cx);
+                })
+        })?;
+
+        Ok(proto::OpenBufferResponse {
+            buffer_id: buffer_id.to_proto(),
+        })
+    }
+
+    fn repository_for_request(
+        this: &Entity<Self>,
+        worktree_id: WorktreeId,
+        work_directory_id: ProjectEntryId,
+        cx: &mut AsyncApp,
+    ) -> Result<RepositoryHandle> {
+        this.update(cx, |project, cx| {
             let repository_handle = project
                 .git_state
                 .read(cx)
@@ -718,20 +793,7 @@ impl HeadlessProject {
                 })
                 .context("missing repository handle")?;
             anyhow::Ok(repository_handle)
-        })??;
-
-        let commit_message = envelope.payload.message;
-        let name = envelope.payload.name.map(SharedString::from);
-        let email = envelope.payload.email.map(SharedString::from);
-        let (err_sender, mut err_receiver) = mpsc::channel(1);
-        repository_handle
-            .commit_with_message(commit_message, name.zip(email), err_sender)
-            .context("unstaging entries")?;
-        if let Some(error) = err_receiver.next().await {
-            Err(error.context("error during unstaging"))
-        } else {
-            Ok(proto::Ack {})
-        }
+        })?
     }
 }