Git tweaks (#28791)

Conrad Irwin , Birk Skyum , and Peter Tripp created

Release Notes:

- git: Add a `git_panel.sort_by_path` setting to mix untracked/tracked
files in the diff list.
- git: Remove the "•" placeholder for "Tracked". The commit button says
"Commit Tracked" still by default, and this was misinterpreted to mean
"partially staged". Hovering over the button will show you which files
are tracked (in addition to the yellow square-with-a-dot-in-it).
- Increase the default value of `expand_excerpt_lines` from 3 to 5. This
makes it faster to see more context in the git diff view.

---------

Co-authored-by: Birk Skyum <birk.skyum@pm.me>
Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

assets/settings/default.json            |   9 
crates/git_ui/src/git_panel.rs          | 395 +++++++++++++-------------
crates/git_ui/src/git_panel_settings.rs |   7 
crates/git_ui/src/project_diff.rs       |  22 +
4 files changed, 229 insertions(+), 204 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -226,7 +226,7 @@
   // Hide the values of in variables from visual display in private files
   "redact_private_values": false,
   // The default number of lines to expand excerpts in the multibuffer by.
-  "expand_excerpt_lines": 3,
+  "expand_excerpt_lines": 5,
   // Globs to match against file paths to determine if a file is private.
   "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
   // Whether to use additional LSP queries to format (and amend) the code after
@@ -601,6 +601,13 @@
     //
     // Default: main
     "fallback_branch_name": "main",
+
+    // Whether to sort entries in the panel by path
+    // or by status (the default).
+    //
+    // Default: false
+    "sort_by_path": false,
+
     "scrollbar": {
       // When to show the scrollbar in the git panel.
       //

crates/git_ui/src/git_panel.rs 🔗

@@ -354,6 +354,7 @@ pub struct GitPanel {
     workspace: WeakEntity<Workspace>,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
     modal_open: bool,
+    show_placeholders: bool,
     _settings_subscription: Subscription,
 }
 
@@ -407,6 +408,16 @@ impl GitPanel {
         })
         .detach();
 
+        let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
+        cx.observe_global::<SettingsStore>(move |this, cx| {
+            let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
+            if is_sort_by_path != was_sort_by_path {
+                this.update_visible_entries(cx);
+            }
+            was_sort_by_path = is_sort_by_path
+        })
+        .detach();
+
         // just to let us render a placeholder editor.
         // Once the active git repo is set, this buffer will be replaced.
         let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
@@ -506,6 +517,7 @@ impl GitPanel {
             tracked_staged_count: 0,
             update_visible_entries_task: Task::ready(()),
             width: None,
+            show_placeholders: false,
             context_menu: None,
             workspace,
             modal_open: false,
@@ -598,7 +610,14 @@ impl GitPanel {
         cx.notify();
     }
 
-    pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
+    pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option<usize> {
+        if GitPanelSettings::get_global(cx).sort_by_path {
+            return self
+                .entries
+                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+                .ok();
+        }
+
         if self.conflicted_count > 0 {
             let conflicted_start = 1;
             if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
@@ -650,7 +669,7 @@ impl GitPanel {
         let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
             return;
         };
-        let Some(ix) = self.entry_by_path(&repo_path) else {
+        let Some(ix) = self.entry_by_path(&repo_path, cx) else {
             return;
         };
         self.selected_entry = Some(ix);
@@ -2294,6 +2313,8 @@ impl GitPanel {
         self.tracked_staged_count = 0;
         self.entry_count = 0;
 
+        let sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
+
         let mut changed_entries = Vec::new();
         let mut new_entries = Vec::new();
         let mut conflict_entries = Vec::new();
@@ -2353,7 +2374,9 @@ impl GitPanel {
                 None => max_width_item = Some((entry.repo_path.clone(), width_estimate)),
             }
 
-            if is_conflict {
+            if sort_by_path {
+                changed_entries.push(entry);
+            } else if is_conflict {
                 conflict_entries.push(entry);
             } else if is_new {
                 new_entries.push(entry);
@@ -2408,9 +2431,11 @@ impl GitPanel {
         }
 
         if changed_entries.len() > 0 {
-            self.entries.push(GitListEntry::Header(GitHeaderEntry {
-                header: Section::Tracked,
-            }));
+            if !sort_by_path {
+                self.entries.push(GitListEntry::Header(GitHeaderEntry {
+                    header: Section::Tracked,
+                }));
+            }
             self.entries.extend(
                 changed_entries
                     .into_iter()
@@ -2464,6 +2489,7 @@ impl GitPanel {
     }
 
     fn update_counts(&mut self, repo: &Repository) {
+        self.show_placeholders = false;
         self.conflicted_count = 0;
         self.conflicted_staged_count = 0;
         self.new_count = 0;
@@ -2525,10 +2551,6 @@ impl GitPanel {
             || self.conflicted_count > self.conflicted_staged_count
     }
 
-    fn has_conflicts(&self) -> bool {
-        self.conflicted_count > 0
-    }
-
     fn has_tracked_changes(&self) -> bool {
         self.tracked_count > 0
     }
@@ -2935,14 +2957,11 @@ impl GitPanel {
         cx: &mut Context<Self>,
     ) -> Option<impl IntoElement> {
         let active_repository = self.active_repository.clone()?;
-        let (can_commit, tooltip) = self.configure_commit_button(cx);
         let panel_editor_style = panel_editor_style(true, window, cx);
 
         let enable_coauthors = self.render_co_authors(cx);
-        let title = self.commit_button_title();
 
         let editor_focus_handle = self.commit_editor.focus_handle(cx);
-        let commit_tooltip_focus_handle = editor_focus_handle.clone();
         let expand_tooltip_focus_handle = editor_focus_handle.clone();
 
         let branch = active_repository.read(cx).branch.clone();
@@ -3010,183 +3029,7 @@ impl GitPanel {
                                 h_flex()
                                     .gap_0p5()
                                     .children(enable_coauthors)
-                                    .when(self.amend_pending, {
-                                        |this| {
-                                            this.h_flex()
-                                                .gap_1()
-                                                .child(
-                                                    panel_filled_button("Cancel")
-                                                        .tooltip({
-                                                            let handle =
-                                                                commit_tooltip_focus_handle.clone();
-                                                            move |window, cx| {
-                                                                Tooltip::for_action_in(
-                                                                    "Cancel amend",
-                                                                    &git::Cancel,
-                                                                    &handle,
-                                                                    window,
-                                                                    cx,
-                                                                )
-                                                            }
-                                                        })
-                                                        .on_click(move |_, window, cx| {
-                                                            window.dispatch_action(
-                                                                Box::new(git::Cancel),
-                                                                cx,
-                                                            );
-                                                        }),
-                                                )
-                                                .child(
-                                                    panel_filled_button(title)
-                                                        .tooltip({
-                                                            let handle =
-                                                                commit_tooltip_focus_handle.clone();
-                                                            move |window, cx| {
-                                                                if can_commit {
-                                                                    Tooltip::for_action_in(
-                                                                        tooltip, &Amend, &handle,
-                                                                        window, cx,
-                                                                    )
-                                                                } else {
-                                                                    Tooltip::simple(tooltip, cx)
-                                                                }
-                                                            }
-                                                        })
-                                                        .disabled(!can_commit || self.modal_open)
-                                                        .on_click({
-                                                            let git_panel = git_panel.downgrade();
-                                                            move |_, window, cx| {
-                                                                telemetry::event!(
-                                                                    "Git Amended",
-                                                                    source = "Git Panel"
-                                                                );
-                                                                git_panel
-                                                                    .update(cx, |git_panel, cx| {
-                                                                        git_panel
-                                                                            .set_amend_pending(
-                                                                                false, cx,
-                                                                            );
-                                                                        git_panel.commit_changes(
-                                                                            CommitOptions {
-                                                                                amend: true,
-                                                                            },
-                                                                            window,
-                                                                            cx,
-                                                                        );
-                                                                    })
-                                                                    .ok();
-                                                            }
-                                                        }),
-                                                )
-                                        }
-                                    })
-                                    .when(!self.amend_pending, |this| {
-                                        this.when(has_previous_commit, |this| {
-                                            this.child(SplitButton::new(
-                                                ui::ButtonLike::new_rounded_left(ElementId::Name(
-                                                    format!("split-button-left-{}", title).into(),
-                                                ))
-                                                .layer(ui::ElevationIndex::ModalSurface)
-                                                .size(ui::ButtonSize::Compact)
-                                                .child(
-                                                    div()
-                                                        .child(
-                                                            Label::new(title)
-                                                                .size(LabelSize::Small),
-                                                        )
-                                                        .mr_0p5(),
-                                                )
-                                                .on_click({
-                                                    let git_panel = git_panel.downgrade();
-                                                    move |_, window, cx| {
-                                                        telemetry::event!(
-                                                            "Git Committed",
-                                                            source = "Git Panel"
-                                                        );
-                                                        git_panel
-                                                            .update(cx, |git_panel, cx| {
-                                                                git_panel.commit_changes(
-                                                                    CommitOptions { amend: false },
-                                                                    window,
-                                                                    cx,
-                                                                );
-                                                            })
-                                                            .ok();
-                                                    }
-                                                })
-                                                .disabled(!can_commit || self.modal_open)
-                                                .tooltip({
-                                                    let handle =
-                                                        commit_tooltip_focus_handle.clone();
-                                                    move |window, cx| {
-                                                        if can_commit {
-                                                            Tooltip::with_meta_in(
-                                                                tooltip,
-                                                                Some(&git::Commit),
-                                                                "git commit",
-                                                                &handle.clone(),
-                                                                window,
-                                                                cx,
-                                                            )
-                                                        } else {
-                                                            Tooltip::simple(tooltip, cx)
-                                                        }
-                                                    }
-                                                }),
-                                                self.render_git_commit_menu(
-                                                    ElementId::Name(
-                                                        format!("split-button-right-{}", title)
-                                                            .into(),
-                                                    ),
-                                                    Some(commit_tooltip_focus_handle.clone()),
-                                                )
-                                                .into_any_element(),
-                                            ))
-                                        })
-                                        .when(
-                                            !has_previous_commit,
-                                            |this| {
-                                                this.child(
-                                                    panel_filled_button(title)
-                                                        .tooltip(move |window, cx| {
-                                                            if can_commit {
-                                                                Tooltip::with_meta_in(
-                                                                    tooltip,
-                                                                    Some(&git::Commit),
-                                                                    "git commit",
-                                                                    &commit_tooltip_focus_handle,
-                                                                    window,
-                                                                    cx,
-                                                                )
-                                                            } else {
-                                                                Tooltip::simple(tooltip, cx)
-                                                            }
-                                                        })
-                                                        .disabled(!can_commit || self.modal_open)
-                                                        .on_click({
-                                                            let git_panel = git_panel.downgrade();
-                                                            move |_, window, cx| {
-                                                                telemetry::event!(
-                                                                    "Git Committed",
-                                                                    source = "Git Panel"
-                                                                );
-                                                                git_panel
-                                                                    .update(cx, |git_panel, cx| {
-                                                                        git_panel.commit_changes(
-                                                                            CommitOptions {
-                                                                                amend: false,
-                                                                            },
-                                                                            window,
-                                                                            cx,
-                                                                        );
-                                                                    })
-                                                                    .ok();
-                                                            }
-                                                        }),
-                                                )
-                                            },
-                                        )
-                                    }),
+                                    .child(self.render_commit_button(has_previous_commit, cx)),
                             ),
                     )
                     .child(
@@ -3235,6 +3078,168 @@ impl GitPanel {
         Some(footer)
     }
 
+    fn render_commit_button(
+        &self,
+        has_previous_commit: bool,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let (can_commit, tooltip) = self.configure_commit_button(cx);
+        let title = self.commit_button_title();
+        let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
+        div()
+            .id("commit-wrapper")
+            .on_hover(cx.listener(move |this, hovered, _, cx| {
+                this.show_placeholders =
+                    *hovered && !this.has_staged_changes() && !this.has_unstaged_conflicts();
+                cx.notify()
+            }))
+            .when(self.amend_pending, {
+                |this| {
+                    this.h_flex()
+                        .gap_1()
+                        .child(
+                            panel_filled_button("Cancel")
+                                .tooltip({
+                                    let handle = commit_tooltip_focus_handle.clone();
+                                    move |window, cx| {
+                                        Tooltip::for_action_in(
+                                            "Cancel amend",
+                                            &git::Cancel,
+                                            &handle,
+                                            window,
+                                            cx,
+                                        )
+                                    }
+                                })
+                                .on_click(move |_, window, cx| {
+                                    window.dispatch_action(Box::new(git::Cancel), cx);
+                                }),
+                        )
+                        .child(
+                            panel_filled_button(title)
+                                .tooltip({
+                                    let handle = commit_tooltip_focus_handle.clone();
+                                    move |window, cx| {
+                                        if can_commit {
+                                            Tooltip::for_action_in(
+                                                tooltip, &Amend, &handle, window, cx,
+                                            )
+                                        } else {
+                                            Tooltip::simple(tooltip, cx)
+                                        }
+                                    }
+                                })
+                                .disabled(!can_commit || self.modal_open)
+                                .on_click({
+                                    let git_panel = cx.weak_entity();
+                                    move |_, window, cx| {
+                                        telemetry::event!("Git Amended", source = "Git Panel");
+                                        git_panel
+                                            .update(cx, |git_panel, cx| {
+                                                git_panel.set_amend_pending(false, cx);
+                                                git_panel.commit_changes(
+                                                    CommitOptions { amend: true },
+                                                    window,
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                    }
+                                }),
+                        )
+                }
+            })
+            .when(!self.amend_pending, |this| {
+                this.when(has_previous_commit, |this| {
+                    this.child(SplitButton::new(
+                        ui::ButtonLike::new_rounded_left(ElementId::Name(
+                            format!("split-button-left-{}", title).into(),
+                        ))
+                        .layer(ui::ElevationIndex::ModalSurface)
+                        .size(ui::ButtonSize::Compact)
+                        .child(
+                            div()
+                                .child(Label::new(title).size(LabelSize::Small))
+                                .mr_0p5(),
+                        )
+                        .on_click({
+                            let git_panel = cx.weak_entity();
+                            move |_, window, cx| {
+                                telemetry::event!("Git Committed", source = "Git Panel");
+                                git_panel
+                                    .update(cx, |git_panel, cx| {
+                                        git_panel.commit_changes(
+                                            CommitOptions { amend: false },
+                                            window,
+                                            cx,
+                                        );
+                                    })
+                                    .ok();
+                            }
+                        })
+                        .disabled(!can_commit || self.modal_open)
+                        .tooltip({
+                            let handle = commit_tooltip_focus_handle.clone();
+                            move |window, cx| {
+                                if can_commit {
+                                    Tooltip::with_meta_in(
+                                        tooltip,
+                                        Some(&git::Commit),
+                                        "git commit",
+                                        &handle.clone(),
+                                        window,
+                                        cx,
+                                    )
+                                } else {
+                                    Tooltip::simple(tooltip, cx)
+                                }
+                            }
+                        }),
+                        self.render_git_commit_menu(
+                            ElementId::Name(format!("split-button-right-{}", title).into()),
+                            Some(commit_tooltip_focus_handle.clone()),
+                        )
+                        .into_any_element(),
+                    ))
+                })
+                .when(!has_previous_commit, |this| {
+                    this.child(
+                        panel_filled_button(title)
+                            .tooltip(move |window, cx| {
+                                if can_commit {
+                                    Tooltip::with_meta_in(
+                                        tooltip,
+                                        Some(&git::Commit),
+                                        "git commit",
+                                        &commit_tooltip_focus_handle,
+                                        window,
+                                        cx,
+                                    )
+                                } else {
+                                    Tooltip::simple(tooltip, cx)
+                                }
+                            })
+                            .disabled(!can_commit || self.modal_open)
+                            .on_click({
+                                let git_panel = cx.weak_entity();
+                                move |_, window, cx| {
+                                    telemetry::event!("Git Committed", source = "Git Panel");
+                                    git_panel
+                                        .update(cx, |git_panel, cx| {
+                                            git_panel.commit_changes(
+                                                CommitOptions { amend: false },
+                                                window,
+                                                cx,
+                                            );
+                                        })
+                                        .ok();
+                                }
+                            }),
+                    )
+                })
+            })
+    }
+
     fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
         div()
             .py_2()
@@ -3482,7 +3487,7 @@ impl GitPanel {
         let repo = self.active_repository.as_ref()?.read(cx);
         let project_path = (file.worktree_id(cx), file.path()).into();
         let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
-        let ix = self.entry_by_path(&repo_path)?;
+        let ix = self.entry_by_path(&repo_path, cx)?;
         let entry = self.entries.get(ix)?;
 
         let entry_staging = self.entry_staging(entry.status_entry()?);
@@ -3852,8 +3857,7 @@ impl GitPanel {
 
         let entry_staging = self.entry_staging(entry);
         let mut is_staged: ToggleState = self.entry_staging(entry).as_bool().into();
-
-        if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() {
+        if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() {
             is_staged = ToggleState::Selected;
         }
 
@@ -3953,11 +3957,6 @@ impl GitPanel {
                         Checkbox::new(checkbox_id, is_staged)
                             .disabled(!has_write_access)
                             .fill()
-                            .placeholder(
-                                !self.has_staged_changes()
-                                    && !self.has_conflicts()
-                                    && !entry.status.is_created(),
-                            )
                             .elevation(ElevationIndex::Surface)
                             .on_click({
                                 let entry = entry.clone();

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -64,6 +64,12 @@ pub struct GitPanelSettingsContent {
     ///
     /// Default: main
     pub fallback_branch_name: Option<String>,
+
+    /// Whether to sort entries in the panel by path
+    /// or by status (the default).
+    ///
+    /// Default: false
+    pub sort_by_path: Option<bool>,
 }
 
 #[derive(Deserialize, Debug, Clone, PartialEq)]
@@ -74,6 +80,7 @@ pub struct GitPanelSettings {
     pub status_style: StatusStyle,
     pub scrollbar: ScrollbarSettings,
     pub fallback_branch_name: String,
+    pub sort_by_path: bool,
 }
 
 impl Settings for GitPanelSettings {

crates/git_ui/src/project_diff.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     conflict_view::ConflictAddon,
     git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
+    git_panel_settings::GitPanelSettings,
     remote_button::{render_publish_button, render_push_button},
 };
 use anyhow::Result;
@@ -27,10 +28,9 @@ use project::{
     Project, ProjectPath,
     git_store::{GitStore, GitStoreEvent, RepositoryEvent},
 };
-use std::{
-    any::{Any, TypeId},
-    ops::Range,
-};
+use settings::{Settings, SettingsStore};
+use std::any::{Any, TypeId};
+use std::ops::Range;
 use theme::ActiveTheme;
 use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
 use util::ResultExt as _;
@@ -165,6 +165,16 @@ impl ProjectDiff {
             },
         );
 
+        let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
+        cx.observe_global::<SettingsStore>(move |this, cx| {
+            let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
+            if is_sort_by_path != was_sort_by_path {
+                *this.update_needed.borrow_mut() = ();
+            }
+            was_sort_by_path = is_sort_by_path
+        })
+        .detach();
+
         let (mut send, recv) = postage::watch::channel::<()>();
         let worker = window.spawn(cx, {
             let this = cx.weak_entity();
@@ -349,7 +359,9 @@ impl ProjectDiff {
                 else {
                     continue;
                 };
-                let namespace = if repo.has_conflict(&entry.repo_path) {
+                let namespace = if GitPanelSettings::get_global(cx).sort_by_path {
+                    TRACKED_NAMESPACE
+                } else if repo.has_conflict(&entry.repo_path) {
                     CONFLICT_NAMESPACE
                 } else if entry.status.is_created() {
                     NEW_NAMESPACE