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

2
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use crate::status::FileStatus; use crate::status::FileStatus;
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus}; use crate::{blame::Blame, status::GitStatus};
use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE};
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use git2::BranchType; 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. /// 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 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 { impl std::fmt::Debug for dyn GitRepository {
@ -283,14 +283,22 @@ impl GitRepository for RealGitRepository {
Ok(()) 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 let working_directory = self
.repository .repository
.lock() .lock()
.workdir() .workdir()
.context("failed to read git work directory")? .context("failed to read git work directory")?
.to_path_buf(); .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}>")); let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
if let Some(author) = author.as_deref() { if let Some(author) = author.as_deref() {
args.push("--author"); args.push("--author");
@ -450,7 +458,7 @@ impl GitRepository for FakeGitRepository {
unimplemented!() unimplemented!()
} }
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> { fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!() unimplemented!()
} }
} }

View file

@ -20,8 +20,10 @@ editor.workspace = true
futures.workspace = true futures.workspace = true
git.workspace = true git.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true
menu.workspace = true menu.workspace = true
project.workspace = true project.workspace = true
rpc.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true

View file

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

View file

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

View file

@ -48,6 +48,7 @@ use ::git::{
blame::Blame, blame::Blame,
repository::{Branch, GitRepository, RepoPath}, repository::{Branch, GitRepository, RepoPath},
status::FileStatus, status::FileStatus,
COMMIT_MESSAGE,
}; };
use gpui::{ use gpui::{
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, 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_stage);
client.add_model_request_handler(Self::handle_unstage); client.add_model_request_handler(Self::handle_unstage);
client.add_model_request_handler(Self::handle_commit); client.add_model_request_handler(Self::handle_commit);
client.add_model_request_handler(Self::handle_open_commit_message_buffer);
WorktreeStore::init(&client); WorktreeStore::init(&client);
BufferStore::init(&client); BufferStore::init(&client);
@ -699,9 +701,7 @@ impl Project {
) )
}); });
let git_state = Some( let git_state = Some(cx.new(|cx| GitState::new(&worktree_store, None, None, cx)));
cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx)),
);
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@ -824,7 +824,6 @@ impl Project {
let git_state = Some(cx.new(|cx| { let git_state = Some(cx.new(|cx| {
GitState::new( GitState::new(
&worktree_store, &worktree_store,
languages.clone(),
Some(ssh_proto.clone()), Some(ssh_proto.clone()),
Some(ProjectId(SSH_PROJECT_ID)), Some(ProjectId(SSH_PROJECT_ID)),
cx, cx,
@ -1030,7 +1029,6 @@ impl Project {
let git_state = Some(cx.new(|cx| { let git_state = Some(cx.new(|cx| {
GitState::new( GitState::new(
&worktree_store, &worktree_store,
languages.clone(),
Some(client.clone().into()), Some(client.clone().into()),
Some(ProjectId(remote_id)), Some(ProjectId(remote_id)),
cx, cx,
@ -3974,21 +3972,8 @@ impl Project {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 =
let repository_handle = project Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
.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 entries = envelope let entries = envelope
.payload .payload
@ -4015,21 +4000,8 @@ impl Project {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 =
let repository_handle = project Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
.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 entries = envelope let entries = envelope
.payload .payload
@ -4056,7 +4028,93 @@ impl Project {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 let repository_handle = project
.git_state() .git_state()
.context("missing git state")? .context("missing git state")?
@ -4070,20 +4128,7 @@ impl Project {
}) })
.context("missing repository handle")?; .context("missing repository handle")?;
anyhow::Ok(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( fn respond_to_open_buffer_request(
@ -4122,7 +4167,7 @@ impl Project {
buffer.read(cx).remote_id() buffer.read(cx).remote_id()
} }
fn wait_for_remote_buffer( pub fn wait_for_remote_buffer(
&mut self, &mut self,
id: BufferId, id: BufferId,
cx: &mut Context<Self>, cx: &mut Context<Self>,

View file

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

View file

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

View file

@ -1,16 +1,16 @@
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use extension::ExtensionHostProxy; use extension::ExtensionHostProxy;
use extension_host::headless_host::HeadlessExtensionStore; use extension_host::headless_host::HeadlessExtensionStore;
use fs::Fs; use fs::{CreateOptions, Fs};
use futures::channel::mpsc; 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 gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString};
use http_client::HttpClient; use http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use project::{ use project::{
buffer_store::{BufferStore, BufferStoreEvent}, buffer_store::{BufferStore, BufferStoreEvent},
git::GitState, git::{GitRepo, GitState, RepositoryHandle},
project_settings::SettingsObserver, project_settings::SettingsObserver,
search::SearchQuery, search::SearchQuery,
task_store::TaskStore, task_store::TaskStore,
@ -83,8 +83,7 @@ impl HeadlessProject {
store store
}); });
let git_state = let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx));
cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx));
let buffer_store = cx.new(|cx| { let buffer_store = cx.new(|cx| {
let mut buffer_store = BufferStore::local(worktree_store.clone(), 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_stage);
client.add_model_request_handler(Self::handle_unstage); client.add_model_request_handler(Self::handle_unstage);
client.add_model_request_handler(Self::handle_commit); client.add_model_request_handler(Self::handle_commit);
client.add_model_request_handler(Self::handle_open_commit_message_buffer);
client.add_request_handler( client.add_request_handler(
extensions.clone().downgrade(), extensions.clone().downgrade(),
@ -625,20 +625,8 @@ impl HeadlessProject {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 =
let repository_handle = project Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
.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 entries = envelope let entries = envelope
.payload .payload
@ -665,20 +653,8 @@ impl HeadlessProject {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 =
let repository_handle = project Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
.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 entries = envelope let entries = envelope
.payload .payload
@ -705,7 +681,106 @@ impl HeadlessProject {
) -> Result<proto::Ack> { ) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_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 let repository_handle = project
.git_state .git_state
.read(cx) .read(cx)
@ -718,20 +793,7 @@ impl HeadlessProject {
}) })
.context("missing repository handle")?; .context("missing repository handle")?;
anyhow::Ok(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 {})
}
} }
} }