Rework shared commit editors (#24274)

Rework of https://github.com/zed-industries/zed/pull/24130
Uses
1033c0b57e
`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>
This commit is contained in:
Kirill Bulatov 2025-02-05 17:36:24 +02:00 committed by GitHub
parent da4bad3a55
commit 868e3f75b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 428 additions and 372 deletions

View file

@ -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<()> {
if entries.is_empty() {
return Ok(());
}
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() {
result_tx.send(Ok(())).ok();
return result_rx;
}
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
}
}

View file

@ -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()
}
}