Fixes to commit button in Git Panel (#24425)

Conrad Irwin and Cole Miller created

Git Panel updates:

* Fixes commit/commit all button to work (and be disabled correctly in
merge conflict status)
* Updates keyboard shortcuts and sets focus on the button (enter now
does the same as click; tab cycles between editor and change list)


Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

assets/keymaps/default-linux.json |  13 
assets/keymaps/default-macos.json |  22 -
assets/keymaps/vim.json           |   4 
crates/git/src/git.rs             |   3 
crates/git/src/repository.rs      |  34 +-
crates/git_ui/src/git_panel.rs    | 393 +++++++++++++-------------------
crates/git_ui/src/project_diff.rs |   2 
crates/proto/proto/zed.proto      |   1 
8 files changed, 203 insertions(+), 269 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -710,15 +710,20 @@
       "enter": "menu::Confirm",
       "space": "git::ToggleStaged",
       "ctrl-space": "git::StageAll",
-      "ctrl-shift-space": "git::UnstageAll"
+      "ctrl-shift-space": "git::UnstageAll",
+      "tab": "git_panel::FocusEditor",
+      "shift-tab": "git_panel::FocusEditor",
+      "escape": "git_panel::ToggleFocus"
     }
   },
   {
-    "context": "GitPanel && CommitEditor > Editor",
+    "context": "GitPanel > Editor",
     "bindings": {
       "escape": "git_panel::FocusChanges",
-      "ctrl-enter": "git::CommitChanges",
-      "ctrl-shift-enter": "git::CommitAllChanges"
+      "ctrl-enter": "git::Commit",
+      "tab": "git_panel::FocusChanges",
+      "shift-tab": "git_panel::FocusChanges",
+      "alt-up": "git_panel::FocusChanges"
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -715,13 +715,6 @@
       "space": "project_panel::Open"
     }
   },
-  {
-    "context": "GitPanel && !CommitEditor",
-    "use_key_equivalents": true,
-    "bindings": {
-      "escape": "git_panel::Close"
-    }
-  },
   {
     "context": "GitPanel && ChangesList",
     "use_key_equivalents": true,
@@ -734,17 +727,20 @@
       "space": "git::ToggleStaged",
       "cmd-shift-space": "git::StageAll",
       "ctrl-shift-space": "git::UnstageAll",
-      "alt-down": "git_panel::FocusEditor"
+      "alt-down": "git_panel::FocusEditor",
+      "tab": "git_panel::FocusEditor",
+      "shift-tab": "git_panel::FocusEditor",
+      "escape": "git_panel::ToggleFocus"
     }
   },
   {
-    "context": "GitPanel && CommitEditor > Editor",
+    "context": "GitPanel > Editor",
     "use_key_equivalents": true,
     "bindings": {
-      "alt-up": "git_panel::FocusChanges",
-      "escape": "git_panel::FocusChanges",
-      "cmd-enter": "git::CommitChanges",
-      "cmd-alt-enter": "git::CommitAllChanges"
+      "cmd-enter": "git::Commit",
+      "tab": "git_panel::FocusChanges",
+      "shift-tab": "git_panel::FocusChanges",
+      "alt-up": "git_panel::FocusChanges"
     }
   },
   {

assets/keymaps/vim.json 🔗

@@ -567,7 +567,7 @@
     }
   },
   {
-    "context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
+    "context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
     "bindings": {
       // window related commands (ctrl-w X)
       "ctrl-w": null,
@@ -625,7 +625,7 @@
     }
   },
   {
-    "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
+    "context": "GitPanel || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
     "bindings": {
       ":": "command_palette::Toggle",
       "g /": "pane::DeploySearch"

crates/git/src/git.rs 🔗

@@ -38,8 +38,7 @@ actions!(
         StageAll,
         UnstageAll,
         RevertAll,
-        CommitChanges,
-        CommitAllChanges,
+        Commit,
         ClearCommitMessage
     ]
 );

crates/git/src/repository.rs 🔗

@@ -293,13 +293,16 @@ impl GitRepository for RealGitRepository {
             .to_path_buf();
 
         if !paths.is_empty() {
-            let status = new_std_command(&self.git_binary_path)
+            let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
                 .args(["update-index", "--add", "--remove", "--"])
                 .args(paths.iter().map(|p| p.as_ref()))
-                .status()?;
-            if !status.success() {
-                return Err(anyhow!("Failed to stage paths: {status}"));
+                .output()?;
+            if !output.status.success() {
+                return Err(anyhow!(
+                    "Failed to stage paths:\n{}",
+                    String::from_utf8_lossy(&output.stderr)
+                ));
             }
         }
         Ok(())
@@ -314,13 +317,16 @@ impl GitRepository for RealGitRepository {
             .to_path_buf();
 
         if !paths.is_empty() {
-            let cmd = new_std_command(&self.git_binary_path)
+            let output = new_std_command(&self.git_binary_path)
                 .current_dir(&working_directory)
                 .args(["reset", "--quiet", "--"])
                 .args(paths.iter().map(|p| p.as_ref()))
-                .status()?;
-            if !cmd.success() {
-                return Err(anyhow!("Failed to unstage paths: {cmd}"));
+                .output()?;
+            if !output.status.success() {
+                return Err(anyhow!(
+                    "Failed to unstage:\n{}",
+                    String::from_utf8_lossy(&output.stderr)
+                ));
             }
         }
         Ok(())
@@ -340,12 +346,16 @@ impl GitRepository for RealGitRepository {
             args.push(author);
         }
 
-        let cmd = new_std_command(&self.git_binary_path)
+        let output = new_std_command(&self.git_binary_path)
             .current_dir(&working_directory)
             .args(args)
-            .status()?;
-        if !cmd.success() {
-            return Err(anyhow!("Failed to commit: {cmd}"));
+            .output()?;
+
+        if !output.status.success() {
+            return Err(anyhow!(
+                "Failed to commit:\n{}",
+                String::from_utf8_lossy(&output.stderr)
+            ));
         }
         Ok(())
     }

crates/git_ui/src/git_panel.rs 🔗

@@ -4,7 +4,6 @@ use crate::ProjectDiff;
 use crate::{
     git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
 };
-use anyhow::Result;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::actions::MoveToEnd;
@@ -12,7 +11,7 @@ use editor::scroll::ScrollbarAutoHide;
 use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
 use git::repository::RepoPath;
 use git::status::FileStatus;
-use git::{CommitAllChanges, CommitChanges, ToggleStaged};
+use git::{Commit, ToggleStaged};
 use gpui::*;
 use language::{Buffer, File};
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
@@ -26,7 +25,7 @@ use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize}
 use theme::ThemeSettings;
 use ui::{
     prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors,
-    ListHeader, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
+    ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::notifications::{DetachAndPromptErr, NotificationId};
@@ -58,6 +57,17 @@ pub fn init(cx: &mut App) {
             workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
                 workspace.toggle_panel_focus::<GitPanel>(window, cx);
             });
+
+            workspace.register_action(|workspace, _: &Commit, window, cx| {
+                workspace.open_panel::<GitPanel>(window, cx);
+                if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
+                    git_panel
+                        .read(cx)
+                        .commit_editor
+                        .focus_handle(cx)
+                        .focus(window);
+                }
+            });
         },
     )
     .detach();
@@ -156,8 +166,7 @@ pub struct GitPanel {
     entries_by_path: collections::HashMap<RepoPath, usize>,
     width: Option<Pixels>,
     pending: Vec<PendingOperation>,
-    commit_task: Task<Result<()>>,
-    commit_pending: bool,
+    pending_commit: Option<Task<()>>,
 
     conflicted_staged_count: usize,
     conflicted_count: usize,
@@ -269,8 +278,7 @@ impl GitPanel {
                 show_scrollbar: false,
                 hide_scrollbar_task: None,
                 update_visible_entries_task: Task::ready(()),
-                commit_task: Task::ready(Ok(())),
-                commit_pending: false,
+                pending_commit: None,
                 active_repository,
                 scroll_handle,
                 fs,
@@ -308,7 +316,12 @@ impl GitPanel {
         git_panel
     }
 
-    pub fn set_focused_path(&mut self, path: ProjectPath, _: &mut Window, cx: &mut Context<Self>) {
+    pub fn select_entry_by_path(
+        &mut self,
+        path: ProjectPath,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         let Some(git_repo) = self.active_repository.as_ref() else {
             return;
         };
@@ -318,7 +331,6 @@ impl GitPanel {
         let Some(ix) = self.entries_by_path.get(&repo_path) else {
             return;
         };
-
         self.selected_entry = Some(*ix);
         cx.notify();
     }
@@ -555,10 +567,17 @@ impl GitPanel {
         self.selected_entry.and_then(|i| self.entries.get(i))
     }
 
-    fn open_selected(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(entry) = self.selected_entry.and_then(|i| self.entries.get(i)) {
-            self.open_entry(entry, cx);
-        }
+    fn open_selected(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
+        maybe!({
+            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
+
+            self.workspace
+                .update(cx, |workspace, cx| {
+                    ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
+                })
+                .ok()
+        });
+        self.focus_handle.focus(window);
     }
 
     fn toggle_staged_for_entry(
@@ -652,135 +671,89 @@ impl GitPanel {
         }
     }
 
-    fn open_entry(&self, entry: &GitListEntry, cx: &mut Context<Self>) {
-        let Some(status_entry) = entry.status_entry() else {
-            return;
-        };
-        let Some(active_repository) = self.active_repository.as_ref() else {
-            return;
-        };
-        let Some(path) = active_repository
-            .read(cx)
-            .repo_path_to_project_path(&status_entry.repo_path)
-        else {
-            return;
-        };
-        let path_exists = self.project.update(cx, |project, cx| {
-            project.entry_for_path(&path, cx).is_some()
-        });
-        if !path_exists {
-            return;
-        }
-        // TODO maybe move all of this into project?
-        cx.emit(Event::OpenedEntry { path });
-    }
-
     /// Commit all staged changes
-    fn commit_changes(
-        &mut self,
-        _: &git::CommitChanges,
-        name_and_email: Option<(SharedString, SharedString)>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(active_repository) = self.active_repository.clone() else {
-            return;
-        };
-        if !self.has_staged_changes() {
-            self.commit_tracked_changes(&Default::default(), name_and_email, window, cx);
-            return;
-        }
-        let message = self.commit_editor.read(cx).text(cx);
-        if message.trim().is_empty() {
-            return;
+    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
+        let editor = self.commit_editor.read(cx);
+        if editor.is_empty(cx) {
+            if !editor.focus_handle(cx).contains_focused(window, cx) {
+                editor.focus_handle(cx).focus(window);
+                return;
+            }
         }
-        self.commit_pending = true;
-        let commit_editor = self.commit_editor.clone();
-        self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
-            let commit = active_repository.update(&mut cx, |active_repository, _| {
-                active_repository.commit(SharedString::from(message), name_and_email)
-            })?;
-            let result = maybe!(async {
-                commit.await??;
-                cx.update(|window, cx| {
-                    commit_editor.update(cx, |editor, cx| editor.clear(window, cx));
-                })
-            })
-            .await;
 
-            git_panel.update(&mut cx, |git_panel, cx| {
-                git_panel.commit_pending = false;
-                result
-                    .map_err(|e| {
-                        git_panel.show_err_toast(e, cx);
-                    })
-                    .ok();
-            })
-        });
+        self.commit_changes(window, cx)
     }
 
-    /// Commit all changes, regardless of whether they are staged or not
-    fn commit_tracked_changes(
-        &mut self,
-        _: &git::CommitAllChanges,
-        name_and_email: Option<(SharedString, SharedString)>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
+    fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let Some(active_repository) = self.active_repository.clone() else {
             return;
         };
-        if !self.has_staged_changes() || !self.has_tracked_changes() {
+        let error_spawn = |message, window: &mut Window, cx: &mut App| {
+            let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
+            cx.spawn(|_| async move {
+                prompt.await.ok();
+            })
+            .detach();
+        };
+
+        if self.has_unstaged_conflicts() {
+            error_spawn(
+                "There are still conflicts. You must stage these before committing",
+                window,
+                cx,
+            );
             return;
         }
 
         let message = self.commit_editor.read(cx).text(cx);
         if message.trim().is_empty() {
+            self.commit_editor.read(cx).focus_handle(cx).focus(window);
             return;
         }
-        self.commit_pending = true;
-        let commit_editor = self.commit_editor.clone();
-        let tracked_files = self
-            .entries
-            .iter()
-            .filter_map(|entry| entry.status_entry())
-            .filter(|status_entry| {
-                !status_entry.status.is_created() && !status_entry.is_staged.unwrap_or(false)
-            })
-            .map(|status_entry| status_entry.repo_path.clone())
-            .collect::<Vec<_>>();
 
-        self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move {
-            let result = maybe!(async {
-                cx.update(|_, cx| active_repository.read(cx).stage_entries(tracked_files))?
-                    .await??;
-                cx.update(|_, cx| {
-                    active_repository
-                        .read(cx)
-                        .commit(SharedString::from(message), name_and_email)
-                })?
-                .await??;
-                Ok(())
-            })
-            .await;
-            cx.update(|window, cx| match result {
-                Ok(_) => commit_editor.update(cx, |editor, cx| {
-                    editor.clear(window, cx);
-                }),
+        let task = if self.has_staged_changes() {
+            // Repository serializes all git operations, so we can just send a commit immediately
+            let commit_task = active_repository.read(cx).commit(message.into(), None);
+            cx.background_executor()
+                .spawn(async move { commit_task.await? })
+        } else {
+            let changed_files = self
+                .entries
+                .iter()
+                .filter_map(|entry| entry.status_entry())
+                .filter(|status_entry| !status_entry.status.is_created())
+                .map(|status_entry| status_entry.repo_path.clone())
+                .collect::<Vec<_>>();
+
+            if changed_files.is_empty() {
+                error_spawn("No changes to commit", window, cx);
+                return;
+            }
 
-                Err(e) => {
-                    git_panel
-                        .update(cx, |git_panel, cx| {
-                            git_panel.show_err_toast(e, cx);
-                        })
-                        .ok();
+            let stage_task = active_repository.read(cx).stage_entries(changed_files);
+            cx.spawn(|_, mut cx| async move {
+                stage_task.await??;
+                let commit_task = active_repository
+                    .update(&mut cx, |repo, _| repo.commit(message.into(), None))?;
+                commit_task.await?
+            })
+        };
+        let task = cx.spawn_in(window, |this, mut cx| async move {
+            let result = task.await;
+            this.update_in(&mut cx, |this, window, cx| {
+                this.pending_commit.take();
+                match result {
+                    Ok(()) => {
+                        this.commit_editor
+                            .update(cx, |editor, cx| editor.clear(window, cx));
+                    }
+                    Err(e) => this.show_err_toast(e, cx),
                 }
-            })?;
-
-            git_panel.update(&mut cx, |git_panel, _| {
-                git_panel.commit_pending = false;
             })
+            .ok();
         });
+
+        self.pending_commit = Some(task);
     }
 
     fn fill_co_authors(&mut self, _: &FillCoAuthors, window: &mut Window, cx: &mut Context<Self>) {
@@ -1057,13 +1030,19 @@ impl GitPanel {
     }
 
     fn has_staged_changes(&self) -> bool {
-        self.tracked_staged_count > 0 || self.new_staged_count > 0
+        self.tracked_staged_count > 0
+            || self.new_staged_count > 0
+            || self.conflicted_staged_count > 0
     }
 
     fn has_tracked_changes(&self) -> bool {
         self.tracked_count > 0
     }
 
+    fn has_unstaged_conflicts(&self) -> bool {
+        self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
+    }
+
     fn header_state(&self, header_type: Section) -> ToggleState {
         let (staged_count, count) = match header_type {
             Section::New => (self.new_staged_count, self.new_count),
@@ -1084,6 +1063,7 @@ impl GitPanel {
             return;
         };
         let notif_id = NotificationId::Named("git-operation-error".into());
+
         let message = e.to_string();
         workspace.update(cx, |workspace, cx| {
             let toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
@@ -1092,9 +1072,7 @@ impl GitPanel {
             workspace.show_toast(toast, cx);
         });
     }
-}
 
-impl GitPanel {
     pub fn panel_button(
         &self,
         id: impl Into<SharedString>,
@@ -1200,14 +1178,13 @@ impl GitPanel {
         )
     }
 
-    pub fn render_commit_editor(
-        &self,
-        name_and_email: Option<(SharedString, SharedString)>,
-        cx: &Context<Self>,
-    ) -> impl IntoElement {
+    pub fn render_commit_editor(&self, cx: &Context<Self>) -> impl IntoElement {
         let editor = self.commit_editor.clone();
-        let can_commit =
-            (self.has_staged_changes() || self.has_tracked_changes()) && !self.commit_pending;
+        let can_commit = (self.has_staged_changes() || self.has_tracked_changes())
+            && self.pending_commit.is_none()
+            && !editor.read(cx).is_empty(cx)
+            && !self.has_unstaged_conflicts()
+            && self.has_write_access(cx);
         let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
 
         let focus_handle_1 = self.focus_handle(cx).clone();
@@ -1226,14 +1203,11 @@ impl GitPanel {
             .panel_button("commit-changes", title)
             .tooltip(move |window, cx| {
                 let focus_handle = focus_handle_1.clone();
-                Tooltip::for_action_in(tooltip, &CommitChanges, &focus_handle, window, cx)
+                Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
             })
             .disabled(!can_commit)
             .on_click({
-                let name_and_email = name_and_email.clone();
-                cx.listener(move |this, _: &ClickEvent, window, cx| {
-                    this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx)
-                })
+                cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx))
             });
 
         div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
@@ -1488,9 +1462,10 @@ impl GitPanel {
         ix: usize,
         header: &GitHeaderEntry,
         has_write_access: bool,
-        _window: &Window,
+        window: &Window,
         cx: &Context<Self>,
     ) -> AnyElement {
+        let selected = self.selected_entry == Some(ix);
         let header_state = if self.has_staged_changes() {
             self.header_state(header.header)
         } else {
@@ -1499,34 +1474,46 @@ impl GitPanel {
                 Section::New => ToggleState::Unselected,
             }
         };
-        let checkbox = Checkbox::new(header.title(), header_state)
+
+        let checkbox = Checkbox::new(("checkbox", ix), header_state)
             .disabled(!has_write_access)
-            .placeholder(!self.has_staged_changes())
             .fill()
-            .elevation(ElevationIndex::Surface);
-        let selected = self.selected_entry == Some(ix);
+            .placeholder(!self.has_staged_changes())
+            .elevation(ElevationIndex::Surface)
+            .on_click({
+                let header = header.clone();
+                cx.listener(move |this, _, window, cx| {
+                    this.toggle_staged_for_entry(&GitListEntry::Header(header.clone()), window, cx);
+                    cx.stop_propagation();
+                })
+            });
+
+        let start_slot = h_flex()
+            .id(("start-slot", ix))
+            .gap(DynamicSpacing::Base04.rems(cx))
+            .child(checkbox)
+            .tooltip(|window, cx| Tooltip::for_action("Stage File", &ToggleStaged, window, cx))
+            .on_mouse_down(MouseButton::Left, |_, _, cx| {
+                // prevent the list item active state triggering when toggling checkbox
+                cx.stop_propagation();
+            });
 
         div()
             .w_full()
             .child(
-                ListHeader::new(header.title())
-                    .start_slot(checkbox)
+                ListItem::new(ix)
+                    .spacing(ListItemSpacing::Sparse)
+                    .start_slot(start_slot)
                     .toggle_state(selected)
-                    .on_toggle({
-                        let header = header.clone();
-                        cx.listener(move |this, _, window, cx| {
-                            if !has_write_access {
-                                return;
-                            }
+                    .focused(selected && self.focus_handle.is_focused(window))
+                    .disabled(!has_write_access)
+                    .on_click({
+                        cx.listener(move |this, _, _, cx| {
                             this.selected_entry = Some(ix);
-                            this.toggle_staged_for_entry(
-                                &GitListEntry::Header(header.clone()),
-                                window,
-                                cx,
-                            )
+                            cx.notify();
                         })
                     })
-                    .inset(true),
+                    .child(h_flex().child(self.entry_label(header.title(), Color::Muted))),
             )
             .into_any_element()
     }
@@ -1614,7 +1601,6 @@ impl GitPanel {
 
         div()
             .w_full()
-            .px_0p5()
             .child(
                 ListItem::new(id)
                     .indent_level(1)
@@ -1622,17 +1608,13 @@ impl GitPanel {
                     .spacing(ListItemSpacing::Sparse)
                     .start_slot(start_slot)
                     .toggle_state(selected)
+                    .focused(selected && self.focus_handle.is_focused(window))
                     .disabled(!has_write_access)
                     .on_click({
-                        let entry = entry.clone();
                         cx.listener(move |this, _, window, cx| {
                             this.selected_entry = Some(ix);
-                            let Some(workspace) = this.workspace.upgrade() else {
-                                return;
-                            };
-                            workspace.update(cx, |workspace, cx| {
-                                ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
-                            })
+                            cx.notify();
+                            this.open_selected(&Default::default(), window, cx);
                         })
                     })
                     .child(
@@ -1660,13 +1642,7 @@ impl GitPanel {
     }
 
     fn has_write_access(&self, cx: &App) -> bool {
-        let room = self
-            .workspace
-            .upgrade()
-            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned());
-
-        room.as_ref()
-            .map_or(true, |room| room.read(cx).local_participant().can_write())
+        !self.project.read(cx).is_read_only(cx)
     }
 }
 
@@ -1684,43 +1660,14 @@ impl Render for GitPanel {
             .upgrade()
             .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned());
 
-        let has_write_access = room
-            .as_ref()
-            .map_or(true, |room| room.read(cx).local_participant().can_write());
-        let (can_commit, name_and_email) = match &room {
-            Some(room) => {
-                if project.is_via_collab() {
-                    if has_write_access {
-                        let name_and_email =
-                            room.read(cx).local_participant_user(cx).and_then(|user| {
-                                let email = SharedString::from(user.email.clone()?);
-                                let name = user
-                                    .name
-                                    .clone()
-                                    .map(SharedString::from)
-                                    .unwrap_or(SharedString::from(user.github_login.clone()));
-                                Some((name, email))
-                            });
-                        (name_and_email.is_some(), name_and_email)
-                    } else {
-                        (false, None)
-                    }
-                } else {
-                    (has_write_access, None)
-                }
-            }
-            None => (has_write_access, None),
-        };
-        let can_commit = !self.commit_pending && can_commit;
-
-        let has_co_authors = can_commit
-            && has_write_access
-            && room.map_or(false, |room| {
-                room.read(cx)
-                    .remote_participants()
-                    .values()
-                    .any(|remote_participant| remote_participant.can_write())
-            });
+        let has_write_access = self.has_write_access(cx);
+
+        let has_co_authors = room.map_or(false, |room| {
+            room.read(cx)
+                .remote_participants()
+                .values()
+                .any(|remote_participant| remote_participant.can_write())
+        });
 
         v_flex()
             .id("git_panel")
@@ -1731,31 +1678,7 @@ impl Render for GitPanel {
                 this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
                     this.toggle_staged_for_selected(&ToggleStaged, window, cx)
                 }))
-                .when(can_commit, |git_panel| {
-                    git_panel
-                        .on_action({
-                            let name_and_email = name_and_email.clone();
-                            cx.listener(move |git_panel, &CommitChanges, window, cx| {
-                                git_panel.commit_changes(
-                                    &CommitChanges,
-                                    name_and_email.clone(),
-                                    window,
-                                    cx,
-                                )
-                            })
-                        })
-                        .on_action({
-                            let name_and_email = name_and_email.clone();
-                            cx.listener(move |git_panel, &CommitAllChanges, window, cx| {
-                                git_panel.commit_tracked_changes(
-                                    &CommitAllChanges,
-                                    name_and_email.clone(),
-                                    window,
-                                    cx,
-                                )
-                            })
-                        })
-                })
+                .on_action(cx.listener(GitPanel::commit))
             })
             .when(self.is_focused(window, cx), |this| {
                 this.on_action(cx.listener(Self::select_first))
@@ -1768,7 +1691,7 @@ impl Render for GitPanel {
             .on_action(cx.listener(Self::focus_changes_list))
             .on_action(cx.listener(Self::focus_editor))
             .on_action(cx.listener(Self::toggle_staged_for_selected))
-            .when(has_co_authors, |git_panel| {
+            .when(has_write_access && has_co_authors, |git_panel| {
                 git_panel.on_action(cx.listener(Self::fill_co_authors))
             })
             // .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
@@ -1791,7 +1714,7 @@ impl Render for GitPanel {
             } else {
                 self.render_empty_state(cx).into_any_element()
             })
-            .child(self.render_commit_editor(name_and_email, cx))
+            .child(self.render_commit_editor(cx))
     }
 }
 

crates/git_ui/src/project_diff.rs 🔗

@@ -235,7 +235,7 @@ impl ProjectDiff {
                     .update(cx, |workspace, cx| {
                         if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
                             git_panel.update(cx, |git_panel, cx| {
-                                git_panel.set_focused_path(project_path.into(), window, cx)
+                                git_panel.select_entry_by_path(project_path.into(), window, cx)
                             })
                         }
                     })

crates/proto/proto/zed.proto 🔗

@@ -368,6 +368,7 @@ enum ErrorCode {
     DevServerProjectPathDoesNotExist = 16;
     RemoteUpgradeRequired = 17;
     RateLimitExceeded = 18;
+    CommitFailed = 19;
     reserved 6;
     reserved 14 to 15;
 }