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:
parent
6b48a6e690
commit
a864168c27
11 changed files with 595 additions and 364 deletions
|
@ -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;
|
||||
}
|
||||
active_repository.commit(name_and_email, self.err_sender.clone(), cx);
|
||||
if self.commit_editor.read(cx).is_empty(cx) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
active_repository.commit_all(name_and_email, self.err_sender.clone(), cx);
|
||||
if self.commit_editor.read(cx).is_empty(cx) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue