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

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>
This commit is contained in:
Kirill Bulatov 2025-02-03 18:11:13 +02:00 committed by GitHub
parent 6b48a6e690
commit a864168c27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 595 additions and 364 deletions

View file

@ -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>,