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:
parent
da4bad3a55
commit
868e3f75b2
17 changed files with 428 additions and 372 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue