Cargo.lock π
@@ -5264,9 +5264,11 @@ dependencies = [
"futures 0.3.31",
"git",
"gpui",
+ "language",
"menu",
"picker",
"project",
+ "rpc",
"schemars",
"serde",
"serde_derive",
Kirill Bulatov and Cole Miller created
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>
Cargo.lock | 2
crates/collab/src/rpc.rs | 1
crates/editor/src/editor.rs | 4
crates/git/src/repository.rs | 18
crates/git_ui/Cargo.toml | 2
crates/git_ui/src/git_panel.rs | 236 ++++++++++++-
crates/project/src/git.rs | 372 ++++++++-------------
crates/project/src/project.rs | 147 +++++---
crates/proto/proto/zed.proto | 14
crates/proto/src/proto.rs | 3
crates/remote_server/src/headless_project.rs | 158 ++++++--
11 files changed, 594 insertions(+), 363 deletions(-)
@@ -5264,9 +5264,11 @@ dependencies = [
"futures 0.3.31",
"git",
"gpui",
+ "language",
"menu",
"picker",
"project",
+ "rpc",
"schemars",
"serde",
"serde_derive",
@@ -394,6 +394,7 @@ impl Server {
.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::Commit>)
+ .add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
.add_request_handler({
@@ -12060,6 +12060,10 @@ impl Editor {
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> {
let text = self.text(cx);
let text = text.trim();
@@ -1,6 +1,6 @@
use crate::status::FileStatus;
-use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
+use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE};
use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet};
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.
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 {
@@ -283,14 +283,22 @@ impl GitRepository for RealGitRepository {
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
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.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}>"));
if let Some(author) = author.as_deref() {
args.push("--author");
@@ -450,7 +458,7 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
- fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
+ fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
}
@@ -20,8 +20,10 @@ editor.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
+language.workspace = true
menu.workspace = true
project.workspace = true
+rpc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
@@ -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;
+ }
+ if self.commit_editor.read(cx).is_empty(cx) {
return;
}
- active_repository.commit(name_and_email, self.err_sender.clone(), cx);
+ 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;
+ }
+ if self.commit_editor.read(cx).is_empty(cx) {
return;
}
- active_repository.commit_all(name_and_email, self.err_sender.clone(), cx);
+ 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
@@ -8,14 +8,10 @@ use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
-use gpui::{
- App, AppContext as _, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity,
-};
-use language::{Buffer, LanguageRegistry};
+use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity};
use rpc::{proto, AnyProtoClient};
use settings::WorktreeId;
use std::sync::Arc;
-use text::Rope;
use util::maybe;
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry};
@@ -25,7 +21,6 @@ pub struct GitState {
repositories: Vec<RepositoryHandle>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
- languages: Arc<LanguageRegistry>,
_subscription: Subscription,
}
@@ -34,13 +29,12 @@ pub struct RepositoryHandle {
git_state: WeakEntity<GitState>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
- git_repo: Option<GitRepo>,
- commit_message: Entity<Buffer>,
+ pub git_repo: GitRepo,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
}
#[derive(Clone)]
-enum GitRepo {
+pub enum GitRepo {
Local(Arc<dyn GitRepository>),
Remote {
project_id: ProjectId,
@@ -70,12 +64,10 @@ enum Message {
StageAndCommit {
git_repo: GitRepo,
paths: Vec<RepoPath>,
- message: Rope,
name_and_email: Option<(SharedString, SharedString)>,
},
Commit {
git_repo: GitRepo,
- message: Rope,
name_and_email: Option<(SharedString, SharedString)>,
},
Stage(GitRepo, Vec<RepoPath>),
@@ -91,7 +83,6 @@ impl EventEmitter<Event> for GitState {}
impl GitState {
pub fn new(
worktree_store: &Entity<WorktreeStore>,
- languages: Arc<LanguageRegistry>,
client: Option<AnyProtoClient>,
project_id: Option<ProjectId>,
cx: &mut Context<'_, Self>,
@@ -100,150 +91,140 @@ impl GitState {
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, mut err_sender)) = update_receiver.next().await {
- let result = cx
- .background_executor()
- .spawn(async move {
- match msg {
- Message::StageAndCommit {
- git_repo,
- message,
- name_and_email,
- paths,
- } => {
- match git_repo {
- GitRepo::Local(repo) => {
- repo.stage_paths(&paths)?;
- repo.commit(
- &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(),
- message: message.to_string(),
- name: name.map(String::from),
- email: email.map(String::from),
- })
- .await
- .context("sending commit request")?;
+ let result =
+ cx.background_executor()
+ .spawn(async move {
+ match msg {
+ Message::StageAndCommit {
+ git_repo,
+ name_and_email,
+ paths,
+ } => {
+ match git_repo {
+ GitRepo::Local(repo) => {
+ repo.stage_paths(&paths)?;
+ 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,
+ } => {
+ 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")?;
+ }
}
- }
- 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 {
+ 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(())
}
- Ok(())
- }
- Message::Unstage(repo, paths) => {
- match repo {
- GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
- GitRepo::Remote {
- project_id,
- client,
- worktree_id,
- work_directory_id,
- } => {
- client
- .request(proto::Unstage {
- 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 unstage request")?;
+ Message::Unstage(repo, paths) => {
+ match repo {
+ GitRepo::Local(repo) => repo.unstage_paths(&paths)?,
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => {
+ client
+ .request(proto::Unstage {
+ 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 unstage request")?;
+ }
}
+ Ok(())
}
- Ok(())
- }
- Message::Commit {
- git_repo,
- message,
- name_and_email,
- } => {
- match git_repo {
- GitRepo::Local(repo) => repo.commit(
- &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,
- } => {
- 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(),
- // 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")?;
+ 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 {
err_sender.send(e).await.ok();
}
@@ -255,7 +236,6 @@ impl GitState {
GitState {
project_id,
- languages,
client,
repositories: Vec::new(),
active_index: None,
@@ -285,7 +265,7 @@ impl GitState {
worktree_store.update(cx, |worktree_store, cx| {
for worktree in worktree_store.worktrees() {
- worktree.update(cx, |worktree, cx| {
+ worktree.update(cx, |worktree, _| {
let snapshot = worktree.snapshot();
for repo in snapshot.repositories().iter() {
let git_repo = worktree
@@ -303,6 +283,9 @@ impl GitState {
work_directory_id: repo.work_directory_id(),
})
});
+ let Some(git_repo) = git_repo else {
+ continue;
+ };
let existing = self
.repositories
.iter()
@@ -317,25 +300,11 @@ impl GitState {
existing_handle.repository_entry = repo.clone();
existing_handle
} 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 {
git_state: this.clone(),
worktree_id: worktree.id(),
repository_entry: repo.clone(),
git_repo,
- commit_message,
update_sender: self.update_sender.clone(),
}
};
@@ -403,10 +372,6 @@ impl RepositoryHandle {
Some((self.worktree_id, path).into())
}
- pub fn commit_message(&self) -> Entity<Buffer> {
- self.commit_message.clone()
- }
-
pub fn stage_entries(
&self,
entries: Vec<RepoPath>,
@@ -415,11 +380,8 @@ impl RepositoryHandle {
if entries.is_empty() {
return Ok(());
}
- let Some(git_repo) = self.git_repo.clone() else {
- return Ok(());
- };
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"))?;
Ok(())
}
@@ -432,11 +394,8 @@ impl RepositoryHandle {
if entries.is_empty() {
return Ok(());
}
- let Some(git_repo) = self.git_repo.clone() else {
- return Ok(());
- };
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"))?;
Ok(())
}
@@ -477,14 +436,8 @@ impl RepositoryHandle {
self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
}
- pub fn can_commit(&self, commit_all: bool, cx: &App) -> bool {
- return self
- .commit_message
- .read(cx)
- .chars()
- .any(|c| !c.is_ascii_whitespace())
- && self.have_changes()
- && (commit_all || self.have_staged_changes());
+ pub fn can_commit(&self, commit_all: bool) -> bool {
+ return self.have_changes() && (commit_all || self.have_staged_changes());
}
pub fn commit(
@@ -492,15 +445,10 @@ impl RepositoryHandle {
name_and_email: Option<(SharedString, SharedString)>,
mut err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut App,
- ) {
- let Some(git_repo) = self.git_repo.clone() else {
- return;
- };
- let message = self.commit_message.read(cx).as_rope().clone();
+ ) -> anyhow::Result<()> {
let result = self.update_sender.unbounded_send((
Message::Commit {
- git_repo,
- message,
+ git_repo: self.git_repo.clone(),
name_and_email,
},
err_sender.clone(),
@@ -513,32 +461,10 @@ impl RepositoryHandle {
.ok();
})
.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(
@@ -546,22 +472,17 @@ impl RepositoryHandle {
name_and_email: Option<(SharedString, SharedString)>,
mut err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut App,
- ) {
- let Some(git_repo) = self.git_repo.clone() else {
- return;
- };
+ ) -> 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();
- let message = self.commit_message.read(cx).as_rope().clone();
let result = self.update_sender.unbounded_send((
Message::StageAndCommit {
- git_repo,
+ git_repo: self.git_repo.clone(),
paths: to_stage,
- message,
name_and_email,
},
err_sender.clone(),
@@ -574,10 +495,9 @@ impl RepositoryHandle {
.ok();
})
.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);
- });
}
}
@@ -48,6 +48,7 @@ use ::git::{
blame::Blame,
repository::{Branch, GitRepository, RepoPath},
status::FileStatus,
+ COMMIT_MESSAGE,
};
use gpui::{
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
@@ -609,6 +610,7 @@ impl Project {
client.add_model_request_handler(Self::handle_stage);
client.add_model_request_handler(Self::handle_unstage);
client.add_model_request_handler(Self::handle_commit);
+ client.add_model_request_handler(Self::handle_open_commit_message_buffer);
WorktreeStore::init(&client);
BufferStore::init(&client);
@@ -699,9 +701,7 @@ impl Project {
)
});
- let git_state = Some(
- cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx)),
- );
+ let git_state = Some(cx.new(|cx| GitState::new(&worktree_store, None, None, cx)));
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@@ -824,7 +824,6 @@ impl Project {
let git_state = Some(cx.new(|cx| {
GitState::new(
&worktree_store,
- languages.clone(),
Some(ssh_proto.clone()),
Some(ProjectId(SSH_PROJECT_ID)),
cx,
@@ -1030,7 +1029,6 @@ impl Project {
let git_state = Some(cx.new(|cx| {
GitState::new(
&worktree_store,
- languages.clone(),
Some(client.clone().into()),
Some(ProjectId(remote_id)),
cx,
@@ -3974,21 +3972,8 @@ impl Project {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
- let repository_handle = project
- .git_state()
- .context("missing git state")?
- .read(cx)
- .all_repositories()
- .into_iter()
- .find(|repository_handle| {
- repository_handle.worktree_id == worktree_id
- && repository_handle.repository_entry.work_directory_id()
- == work_directory_id
- })
- .context("missing repository handle")?;
- anyhow::Ok(repository_handle)
- })??;
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let entries = envelope
.payload
@@ -4015,21 +4000,8 @@ impl Project {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
- let repository_handle = project
- .git_state()
- .context("missing git state")?
- .read(cx)
- .all_repositories()
- .into_iter()
- .find(|repository_handle| {
- repository_handle.worktree_id == worktree_id
- && repository_handle.repository_entry.work_directory_id()
- == work_directory_id
- })
- .context("missing repository handle")?;
- anyhow::Ok(repository_handle)
- })??;
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let entries = envelope
.payload
@@ -4056,7 +4028,93 @@ impl Project {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ let name = envelope.payload.name.map(SharedString::from);
+ let email = envelope.payload.email.map(SharedString::from);
+ let (err_sender, mut err_receiver) = mpsc::channel(1);
+ cx.update(|cx| {
+ repository_handle
+ .commit(name.zip(email), err_sender, cx)
+ .context("unstaging entries")
+ })??;
+ if let Some(error) = err_receiver.next().await {
+ Err(error.context("error during unstaging"))
+ } else {
+ Ok(proto::Ack {})
+ }
+ }
+
+ async fn handle_open_commit_message_buffer(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::OpenBufferResponse> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let git_repository = match &repository_handle.git_repo {
+ git::GitRepo::Local(git_repository) => git_repository.clone(),
+ git::GitRepo::Remote { .. } => {
+ anyhow::bail!("Cannot handle open commit message buffer for remote git repo")
+ }
+ };
+ let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE);
+ let fs = this.update(&mut cx, |project, _| project.fs().clone())?;
+ fs.create_file(
+ &commit_message_file,
+ CreateOptions {
+ overwrite: false,
+ ignore_if_exists: true,
+ },
+ )
+ .await
+ .with_context(|| format!("creating commit message file {commit_message_file:?}"))?;
+
+ let (worktree, relative_path) = this
+ .update(&mut cx, |headless_project, cx| {
+ headless_project
+ .worktree_store
+ .update(cx, |worktree_store, cx| {
+ worktree_store.find_or_create_worktree(&commit_message_file, false, cx)
+ })
+ })?
+ .await
+ .with_context(|| {
+ format!("deriving worktree for commit message file {commit_message_file:?}")
+ })?;
+
+ let buffer = this
+ .update(&mut cx, |headless_project, cx| {
+ headless_project
+ .buffer_store
+ .update(cx, |buffer_store, cx| {
+ buffer_store.open_buffer(
+ ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: Arc::from(relative_path),
+ },
+ cx,
+ )
+ })
+ })
+ .with_context(|| {
+ format!("opening buffer for commit message file {commit_message_file:?}")
+ })?
+ .await?;
+ let peer_id = envelope.original_sender_id()?;
+ Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
+ }
+
+ fn repository_for_request(
+ this: &Entity<Self>,
+ worktree_id: WorktreeId,
+ work_directory_id: ProjectEntryId,
+ cx: &mut AsyncApp,
+ ) -> Result<RepositoryHandle> {
+ this.update(cx, |project, cx| {
let repository_handle = project
.git_state()
.context("missing git state")?
@@ -4070,20 +4128,7 @@ impl Project {
})
.context("missing repository handle")?;
anyhow::Ok(repository_handle)
- })??;
-
- let commit_message = envelope.payload.message;
- let name = envelope.payload.name.map(SharedString::from);
- let email = envelope.payload.email.map(SharedString::from);
- let (err_sender, mut err_receiver) = mpsc::channel(1);
- repository_handle
- .commit_with_message(commit_message, name.zip(email), err_sender)
- .context("unstaging entries")?;
- if let Some(error) = err_receiver.next().await {
- Err(error.context("error during unstaging"))
- } else {
- Ok(proto::Ack {})
- }
+ })?
}
fn respond_to_open_buffer_request(
@@ -4122,7 +4167,7 @@ impl Project {
buffer.read(cx).remote_id()
}
- fn wait_for_remote_buffer(
+ pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
cx: &mut Context<Self>,
@@ -311,7 +311,8 @@ message Envelope {
Stage stage = 293;
Unstage unstage = 294;
- Commit commit = 295; // current max
+ Commit commit = 295;
+ OpenCommitMessageBuffer open_commit_message_buffer = 296; // current max
}
reserved 87 to 88;
@@ -2655,7 +2656,12 @@ message Commit {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
- string message = 4;
- optional string name = 5;
- optional string email = 6;
+ optional string name = 4;
+ optional string email = 5;
+}
+
+message OpenCommitMessageBuffer {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
}
@@ -249,6 +249,7 @@ messages!(
(OpenBufferForSymbol, Background),
(OpenBufferForSymbolResponse, Background),
(OpenBufferResponse, Background),
+ (OpenCommitMessageBuffer, Background),
(PerformRename, Background),
(PerformRenameResponse, Background),
(Ping, Foreground),
@@ -443,6 +444,7 @@ request_messages!(
(OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse),
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
+ (OpenCommitMessageBuffer, OpenBufferResponse),
(OpenNewBuffer, OpenBufferResponse),
(PerformRename, PerformRenameResponse),
(Ping, Ack),
@@ -554,6 +556,7 @@ entity_messages!(
OpenBufferById,
OpenBufferByPath,
OpenBufferForSymbol,
+ OpenCommitMessageBuffer,
PerformRename,
PrepareRename,
RefreshInlayHints,
@@ -1,16 +1,16 @@
use anyhow::{anyhow, Context as _, Result};
use extension::ExtensionHostProxy;
use extension_host::headless_host::HeadlessExtensionStore;
-use fs::Fs;
+use fs::{CreateOptions, Fs};
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 http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime;
use project::{
buffer_store::{BufferStore, BufferStoreEvent},
- git::GitState,
+ git::{GitRepo, GitState, RepositoryHandle},
project_settings::SettingsObserver,
search::SearchQuery,
task_store::TaskStore,
@@ -83,8 +83,7 @@ impl HeadlessProject {
store
});
- let git_state =
- cx.new(|cx| GitState::new(&worktree_store, languages.clone(), None, None, cx));
+ let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx));
let buffer_store = cx.new(|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_unstage);
client.add_model_request_handler(Self::handle_commit);
+ client.add_model_request_handler(Self::handle_open_commit_message_buffer);
client.add_request_handler(
extensions.clone().downgrade(),
@@ -625,20 +625,8 @@ impl HeadlessProject {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
- let repository_handle = project
- .git_state
- .read(cx)
- .all_repositories()
- .into_iter()
- .find(|repository_handle| {
- repository_handle.worktree_id == worktree_id
- && repository_handle.repository_entry.work_directory_id()
- == work_directory_id
- })
- .context("missing repository handle")?;
- anyhow::Ok(repository_handle)
- })??;
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let entries = envelope
.payload
@@ -665,20 +653,8 @@ impl HeadlessProject {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
- let repository_handle = project
- .git_state
- .read(cx)
- .all_repositories()
- .into_iter()
- .find(|repository_handle| {
- repository_handle.worktree_id == worktree_id
- && repository_handle.repository_entry.work_directory_id()
- == work_directory_id
- })
- .context("missing repository handle")?;
- anyhow::Ok(repository_handle)
- })??;
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let entries = envelope
.payload
@@ -705,7 +681,106 @@ impl HeadlessProject {
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
- let repository_handle = this.update(&mut cx, |project, cx| {
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ let name = envelope.payload.name.map(SharedString::from);
+ let email = envelope.payload.email.map(SharedString::from);
+ let (err_sender, mut err_receiver) = mpsc::channel(1);
+ cx.update(|cx| {
+ repository_handle
+ .commit(name.zip(email), err_sender, cx)
+ .context("unstaging entries")
+ })??;
+ if let Some(error) = err_receiver.next().await {
+ Err(error.context("error during unstaging"))
+ } else {
+ Ok(proto::Ack {})
+ }
+ }
+
+ async fn handle_open_commit_message_buffer(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::OpenBufferResponse> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+ let git_repository = match &repository_handle.git_repo {
+ 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
.git_state
.read(cx)
@@ -718,20 +793,7 @@ impl HeadlessProject {
})
.context("missing repository handle")?;
anyhow::Ok(repository_handle)
- })??;
-
- let commit_message = envelope.payload.message;
- let name = envelope.payload.name.map(SharedString::from);
- let email = envelope.payload.email.map(SharedString::from);
- let (err_sender, mut err_receiver) = mpsc::channel(1);
- repository_handle
- .commit_with_message(commit_message, name.zip(email), err_sender)
- .context("unstaging entries")?;
- if let Some(error) = err_receiver.next().await {
- Err(error.context("error during unstaging"))
- } else {
- Ok(proto::Ack {})
- }
+ })?
}
}