git_ui: New panel design (#25821)

Nate Butler , Cole Miller , Cole Miller , and Cole Miller created

This PR updates the ui of the git panel. It removes the header from the
panel and unifies the repository, branch and commit controls in the
bottom section.

It also adds a secondary menu to the primary button giving access to a
variety of actions for managing local and remote changes:

![CleanShot 2025-02-28 at 12 18
15@2x](https://github.com/user-attachments/assets/0260c122-405f-46fc-8cc8-d6beac782b9d)

Known issues (will be fixed in a later pr)
- Spinner showing git operation progress was removed, will be re-added
- Clicking expand with the panel editor focused will commit (due to
shared action name. Already tracked)

Before | After

![CleanShot 2025-02-28 at 12 22
18@2x](https://github.com/user-attachments/assets/4c1e4ac9-b975-487f-bf4e-8815a8da4f4f)

(Also adds `component`, `linkme` to cargo-machete ignore as they are
used in the `IntoComponent` proc-macro and will always be incorrectly
flagged as unused)

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

Cargo.lock                                        |    3 
Cargo.toml                                        |    2 
assets/icons/git_branch_small.svg                 |    6 
crates/component/src/component.rs                 |   23 
crates/component_preview/src/component_preview.rs |    2 
crates/git/src/git.rs                             |    1 
crates/git/src/repository.rs                      |    6 
crates/git_ui/Cargo.toml                          |    3 
crates/git_ui/src/commit_modal.rs                 |   10 
crates/git_ui/src/git_panel.rs                    | 1103 +++++++++++-----
crates/panel/src/panel.rs                         |    2 
crates/ui/src/components/avatar.rs                |    2 
crates/ui/src/components/button/button.rs         |    2 
crates/ui/src/components/button/icon_button.rs    |    2 
crates/ui/src/components/button/toggle_button.rs  |    2 
crates/ui/src/components/content_group.rs         |    2 
crates/ui/src/components/facepile.rs              |    2 
crates/ui/src/components/icon.rs                  |    3 
crates/ui/src/components/icon/decorated_icon.rs   |    2 
crates/ui/src/components/keybinding_hint.rs       |    2 
crates/ui/src/components/label/label.rs           |    2 
crates/ui/src/components/tab.rs                   |    2 
crates/ui/src/components/table.rs                 |    2 
crates/ui/src/components/toggle.rs                |    6 
crates/ui/src/components/tooltip.rs               |    2 
crates/ui/src/styles/typography.rs                |    2 
26 files changed, 828 insertions(+), 368 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5395,6 +5395,7 @@ dependencies = [
  "anyhow",
  "buffer_diff",
  "collections",
+ "component",
  "db",
  "editor",
  "feature_flags",
@@ -5404,6 +5405,7 @@ dependencies = [
  "gpui",
  "itertools 0.14.0",
  "language",
+ "linkme",
  "menu",
  "multi_buffer",
  "panel",
@@ -5415,6 +5417,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "settings",
+ "smallvec",
  "strum",
  "theme",
  "time",

Cargo.toml 🔗

@@ -749,4 +749,4 @@ should_implement_trait = { level = "allow" }
 let_underscore_future = "allow"
 
 [workspace.metadata.cargo-machete]
-ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
+ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]

assets/icons/git_branch_small.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/>
+</svg>

crates/component/src/component.rs 🔗

@@ -18,7 +18,7 @@ pub trait Component {
 }
 
 pub trait ComponentPreview: Component {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement;
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
 }
 
 #[distributed_slice]
@@ -32,7 +32,7 @@ pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
 
 pub struct ComponentRegistry {
     components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
-    previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>,
+    previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
 }
 
 impl ComponentRegistry {
@@ -62,7 +62,10 @@ pub fn register_component<T: Component>() {
 }
 
 pub fn register_preview<T: ComponentPreview>() {
-    let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement);
+    let preview_data = (
+        T::name(),
+        T::preview as fn(&mut Window, &mut App) -> AnyElement,
+    );
     COMPONENT_DATA
         .write()
         .previews
@@ -77,7 +80,7 @@ pub struct ComponentMetadata {
     name: SharedString,
     scope: Option<SharedString>,
     description: Option<SharedString>,
-    preview: Option<fn(&mut Window, &App) -> AnyElement>,
+    preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
 }
 
 impl ComponentMetadata {
@@ -93,7 +96,7 @@ impl ComponentMetadata {
         self.description.clone()
     }
 
-    pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
+    pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> AnyElement> {
         self.preview
     }
 }
@@ -235,6 +238,7 @@ pub struct ComponentExampleGroup {
     pub title: Option<SharedString>,
     pub examples: Vec<ComponentExample>,
     pub grow: bool,
+    pub vertical: bool,
 }
 
 impl RenderOnce for ComponentExampleGroup {
@@ -270,6 +274,7 @@ impl RenderOnce for ComponentExampleGroup {
             .child(
                 div()
                     .flex()
+                    .when(self.vertical, |this| this.flex_col())
                     .items_start()
                     .w_full()
                     .gap_6()
@@ -287,6 +292,7 @@ impl ComponentExampleGroup {
             title: None,
             examples,
             grow: false,
+            vertical: false,
         }
     }
 
@@ -296,6 +302,7 @@ impl ComponentExampleGroup {
             title: Some(title.into()),
             examples,
             grow: false,
+            vertical: false,
         }
     }
 
@@ -304,6 +311,12 @@ impl ComponentExampleGroup {
         self.grow = true;
         self
     }
+
+    /// Lay the group out vertically.
+    pub fn vertical(mut self) -> Self {
+        self.vertical = true;
+        self
+    }
 }
 
 /// Create a single example

crates/git/src/git.rs 🔗

@@ -56,6 +56,7 @@ actions!(
         Pull,
         Fetch,
         Commit,
+        ExpandCommitEditor,
     ]
 );
 action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);

crates/git/src/repository.rs 🔗

@@ -74,6 +74,12 @@ impl UpstreamTracking {
     }
 }
 
+impl From<UpstreamTrackingStatus> for UpstreamTracking {
+    fn from(status: UpstreamTrackingStatus) -> Self {
+        UpstreamTracking::Tracked(status)
+    }
+}
+
 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 pub struct UpstreamTrackingStatus {
     pub ahead: u32,

crates/git_ui/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = ["multi_buffer/test-support"]
 anyhow.workspace = true
 buffer_diff.workspace = true
 collections.workspace = true
+component.workspace = true
 db.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
@@ -29,6 +30,7 @@ git.workspace = true
 gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
+linkme.workspace = true
 menu.workspace = true
 multi_buffer.workspace = true
 panel.workspace = true
@@ -40,6 +42,7 @@ serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+smallvec.workspace = true
 strum.workspace = true
 theme.workspace = true
 time.workspace = true

crates/git_ui/src/commit_modal.rs 🔗

@@ -2,7 +2,7 @@
 
 use crate::branch_picker::{self, BranchList};
 use crate::git_panel::{commit_message_editor, GitPanel};
-use git::Commit;
+use git::{Commit, ExpandCommitEditor};
 use panel::{panel_button, panel_editor_style, panel_filled_button};
 use project::Project;
 use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
@@ -110,14 +110,17 @@ struct RestoreDock {
 
 impl CommitModal {
     pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
-        workspace.register_action(|workspace, _: &Commit, window, cx| {
+        workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {
             let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
                 return;
             };
 
-            let (can_commit, conflict) = git_panel.update(cx, |git_panel, _cx| {
+            let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
                 let can_commit = git_panel.can_commit();
                 let conflict = git_panel.has_unstaged_conflicts();
+                if can_commit {
+                    git_panel.set_modal_open(true, cx);
+                }
                 (can_commit, conflict)
             });
             if !can_commit {
@@ -131,6 +134,7 @@ impl CommitModal {
                     prompt.await.ok();
                 })
                 .detach();
+                return;
             }
 
             let dock = workspace.dock_at_position(git_panel.position(window, cx));

crates/git_ui/src/git_panel.rs 🔗

@@ -1,5 +1,4 @@
 use crate::git_panel_settings::StatusStyle;
-use crate::project_diff::Diff;
 use crate::repository_selector::RepositorySelectorPopoverMenu;
 use crate::{
     git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
@@ -11,21 +10,27 @@ use editor::{
     scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
     ShowScrollbar,
 };
-use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode, UpstreamTracking};
+use git::repository::{
+    Branch, CommitDetails, CommitSummary, PushOptions, Remote, ResetMode, Upstream,
+    UpstreamTracking, UpstreamTrackingStatus,
+};
 use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
-use git::{Push, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
+use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
 use gpui::*;
 use itertools::Itertools;
 use language::{Buffer, File};
 use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
 use multi_buffer::ExcerptInfo;
-use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader};
+use panel::{
+    panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, PanelHeader,
+};
 use project::{
     git::{GitEvent, Repository},
     Fs, Project, ProjectPath,
 };
 use serde::{Deserialize, Serialize};
 use settings::Settings as _;
+use smallvec::smallvec;
 use std::cell::RefCell;
 use std::future::Future;
 use std::rc::Rc;
@@ -33,8 +38,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
 use strum::{IntoEnumIterator, VariantNames};
 use time::OffsetDateTime;
 use ui::{
-    prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
-    ListItemSpacing, PopoverMenu, Scrollbar, ScrollbarState, Tooltip,
+    prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, ListItem, ListItemSpacing,
+    PopoverMenu, Scrollbar, ScrollbarState, Tooltip,
 };
 use util::{maybe, post_inc, ResultExt, TryFutureExt};
 use workspace::{
@@ -70,6 +75,19 @@ enum TrashCancel {
     Cancel,
 }
 
+fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
+    ContextMenu::build(window, cx, |context_menu, _, _| {
+        context_menu
+            .action("Stage All", StageAll.boxed_clone())
+            .action("Unstage All", UnstageAll.boxed_clone())
+            .separator()
+            .action("Open Diff", project_diff::Diff.boxed_clone())
+            .separator()
+            .action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
+            .action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
+    })
+}
+
 const GIT_PANEL_KEY: &str = "GitPanel";
 
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
@@ -1779,88 +1797,7 @@ impl GitPanel {
         });
     }
 
-    pub fn panel_button(
-        &self,
-        id: impl Into<SharedString>,
-        label: impl Into<SharedString>,
-    ) -> Button {
-        let id = id.into().clone();
-        let label = label.into().clone();
-
-        Button::new(id, label)
-            .label_size(LabelSize::Small)
-            .layer(ElevationIndex::ElevatedSurface)
-            .size(ButtonSize::Compact)
-            .style(ButtonStyle::Filled)
-    }
-
-    pub fn indent_size(&self, window: &Window, cx: &mut Context<Self>) -> Pixels {
-        Checkbox::container_size(cx).to_pixels(window.rem_size())
-    }
-
-    pub fn render_divider(&self, _cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
-            .items_center()
-            .h(px(8.))
-            .child(Divider::horizontal_dashed().color(DividerColor::Border))
-    }
-
-    pub fn render_panel_header(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<impl IntoElement> {
-        let all_repositories = self
-            .project
-            .read(cx)
-            .git_store()
-            .read(cx)
-            .all_repositories();
-
-        let has_repo_above = all_repositories.iter().any(|repo| {
-            repo.read(cx)
-                .repository_entry
-                .work_directory
-                .is_above_project()
-        });
-
-        let has_visible_repo = all_repositories.len() > 0 || has_repo_above;
-
-        if has_visible_repo {
-            Some(
-                self.panel_header_container(window, cx)
-                    .child(
-                        Label::new("Repository")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(self.render_repository_selector(cx))
-                    .child(div().flex_grow()) // spacer
-                    .child(
-                        div()
-                            .h_flex()
-                            .gap_1()
-                            .children(self.render_spinner(cx))
-                            .children(self.render_sync_button(cx))
-                            .children(self.render_pull_button(cx))
-                            .child(
-                                Button::new("diff", "+/-")
-                                    .tooltip(Tooltip::for_action_title("Open diff", &Diff))
-                                    .on_click(|_, _, cx| {
-                                        cx.defer(|cx| {
-                                            cx.dispatch_action(&Diff);
-                                        })
-                                    }),
-                            )
-                            .child(self.render_overflow_menu()),
-                    ),
-            )
-        } else {
-            None
-        }
-    }
-
-    pub fn render_spinner(&self, _cx: &mut Context<Self>) -> Option<impl IntoElement> {
+    fn render_spinner(&self) -> Option<impl IntoElement> {
         (!self.pending_remote_operations.borrow().is_empty()).then(|| {
             Icon::new(IconName::ArrowCircle)
                 .size(IconSize::XSmall)
@@ -1874,83 +1811,6 @@ impl GitPanel {
         })
     }
 
-    pub fn render_overflow_menu(&self) -> impl IntoElement {
-        PopoverMenu::new("overflow-menu")
-            .trigger(IconButton::new("overflow-menu-trigger", IconName::Ellipsis))
-            .menu(move |window, cx| Some(Self::panel_context_menu(window, cx)))
-            .anchor(Corner::TopRight)
-    }
-
-    pub fn render_sync_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
-        let active_repository = self.project.read(cx).active_repository(cx);
-        active_repository.as_ref().map(|_| {
-            panel_filled_button("Fetch")
-                .icon(IconName::ArrowCircle)
-                .icon_size(IconSize::Small)
-                .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start)
-                .tooltip(Tooltip::for_action_title("git fetch", &git::Fetch))
-                .on_click(
-                    cx.listener(move |this, _, window, cx| this.fetch(&git::Fetch, window, cx)),
-                )
-                .into_any_element()
-        })
-    }
-
-    pub fn render_pull_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
-        let active_repository = self.project.read(cx).active_repository(cx);
-        active_repository
-            .as_ref()
-            .and_then(|repo| repo.read(cx).current_branch())
-            .and_then(|branch| {
-                branch.upstream.as_ref().map(|upstream| {
-                    let status = &upstream.tracking;
-
-                    let disabled = status.is_gone();
-
-                    panel_filled_button(match status {
-                        git::repository::UpstreamTracking::Tracked(status) if status.behind > 0 => {
-                            format!("Pull ({})", status.behind)
-                        }
-                        _ => "Pull".to_string(),
-                    })
-                    .icon(IconName::ArrowDown)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
-                    .icon_position(IconPosition::Start)
-                    .disabled(status.is_gone())
-                    .tooltip(move |window, cx| {
-                        if disabled {
-                            Tooltip::simple("Upstream is gone", cx)
-                        } else {
-                            // TODO: Add <origin> and <branch> argument substitutions to this
-                            Tooltip::for_action("git pull", &git::Pull, window, cx)
-                        }
-                    })
-                    .on_click(
-                        cx.listener(move |this, _, window, cx| this.pull(&git::Pull, window, cx)),
-                    )
-                    .into_any_element()
-                })
-            })
-    }
-
-    pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        let active_repository = self.project.read(cx).active_repository(cx);
-        let repository_display_name = active_repository
-            .as_ref()
-            .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx))
-            .unwrap_or_default();
-
-        RepositorySelectorPopoverMenu::new(
-            self.repository_selector.clone(),
-            ButtonLike::new("active-repository")
-                .style(ButtonStyle::Subtle)
-                .child(Label::new(repository_display_name).size(LabelSize::Small)),
-            Tooltip::text("Select a repository"),
-        )
-    }
-
     pub fn can_commit(&self) -> bool {
         (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts()
     }
@@ -1997,103 +1857,131 @@ impl GitPanel {
         }
     }
 
-    pub fn render_commit_editor(
+    pub fn render_footer(
         &self,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let editor = self.commit_editor.clone();
-        let can_commit = self.can_commit()
-            && self.pending_commit.is_none()
-            && !editor.read(cx).is_empty(cx)
-            && self.has_write_access(cx);
-
+    ) -> Option<impl IntoElement> {
+        let project = self.project.clone().read(cx);
+        let active_repository = self.active_repository.clone();
         let panel_editor_style = panel_editor_style(true, window, cx);
-        let enable_coauthors = self.render_co_authors(cx);
 
-        let tooltip = if self.has_staged_changes() {
-            "git commit"
-        } else {
-            "git commit --all"
-        };
-        let title = if self.has_staged_changes() {
-            "Commit"
-        } else {
-            "Commit Tracked"
-        };
-        let editor_focus_handle = self.commit_editor.focus_handle(cx);
+        if let Some(active_repo) = active_repository {
+            let editor = self.commit_editor.clone();
+            let can_commit = self.can_commit()
+                && self.pending_commit.is_none()
+                && !editor.read(cx).is_empty(cx)
+                && self.has_write_access(cx);
 
-        let commit_button = panel_filled_button(title)
-            .tooltip(move |window, cx| {
-                Tooltip::for_action_in(tooltip, &Commit, &editor_focus_handle, window, cx)
-            })
-            .disabled(!can_commit)
-            .on_click({
-                cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx))
-            });
+            let enable_coauthors = self.render_co_authors(cx);
 
-        let branch = self
-            .active_repository
-            .as_ref()
-            .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
-            .unwrap_or_else(|| "<no branch>".into());
-
-        let branch_selector = Button::new("branch-selector", branch)
-            .color(Color::Muted)
-            .style(ButtonStyle::Subtle)
-            .icon(IconName::GitBranch)
-            .icon_size(IconSize::Small)
-            .icon_color(Color::Muted)
-            .size(ButtonSize::Compact)
-            .icon_position(IconPosition::Start)
-            .tooltip(Tooltip::for_action_title(
-                "Switch Branch",
-                &zed_actions::git::Branch,
-            ))
-            .on_click(cx.listener(|_, _, window, cx| {
-                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
-            }))
-            .style(ButtonStyle::Transparent);
+            let tooltip = if self.has_staged_changes() {
+                "git commit"
+            } else {
+                "git commit --all"
+            };
+            let title = if self.has_staged_changes() {
+                "Commit"
+            } else {
+                "Commit Tracked"
+            };
+            let editor_focus_handle = self.commit_editor.focus_handle(cx);
 
-        let footer_size = px(32.);
-        let gap = px(16.0);
+            let branch = active_repo.read(cx).current_branch()?;
 
-        let max_height = window.line_height() * 6. + gap + footer_size;
+            let footer_size = px(32.);
+            let gap = px(8.0);
 
-        panel_editor_container(window, cx)
-            .id("commit-editor-container")
-            .relative()
-            .h(max_height)
-            .w_full()
-            .border_t_1()
-            .border_color(cx.theme().colors().border)
-            .bg(cx.theme().colors().editor_background)
-            .cursor_text()
-            .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
-                window.focus(&this.commit_editor.focus_handle(cx));
-            }))
-            .when(!self.modal_open, |el| {
-                el.child(EditorElement::new(&self.commit_editor, panel_editor_style))
-                    .child(
-                        h_flex()
-                            .absolute()
-                            .bottom_0()
-                            .left_2()
-                            .h(footer_size)
-                            .flex_none()
-                            .child(branch_selector),
-                    )
-                    .child(
-                        h_flex()
-                            .absolute()
-                            .bottom_0()
-                            .right_2()
-                            .h(footer_size)
-                            .flex_none()
-                            .children(enable_coauthors)
-                            .child(commit_button),
-                    )
-            })
+            let max_height = window.line_height() * 5. + gap + footer_size;
+
+            let expand_button_size = px(16.);
+
+            let git_panel = cx.entity().clone();
+            let display_name = SharedString::from(Arc::from(
+                active_repo
+                    .read(cx)
+                    .display_name(project, cx)
+                    .trim_end_matches("/"),
+            ));
+            let footer = v_flex()
+                .child(PanelRepoHeader::new(
+                    "header-button",
+                    display_name,
+                    Some(branch.clone()),
+                    Some(git_panel),
+                ))
+                .child(
+                    panel_editor_container(window, cx)
+                        .id("commit-editor-container")
+                        .relative()
+                        .h(max_height)
+                        // .w_full()
+                        // .border_t_1()
+                        // .border_color(cx.theme().colors().border)
+                        .bg(cx.theme().colors().editor_background)
+                        .cursor_text()
+                        .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
+                            window.focus(&this.commit_editor.focus_handle(cx));
+                        }))
+                        .child(
+                            h_flex()
+                                .id("commit-footer")
+                                .absolute()
+                                .bottom_0()
+                                .right_2()
+                                .h(footer_size)
+                                .flex_none()
+                                .children(enable_coauthors)
+                                .child(
+                                    panel_filled_button(title)
+                                        .tooltip(move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                tooltip,
+                                                &Commit,
+                                                &editor_focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        })
+                                        .disabled(!can_commit || self.modal_open)
+                                        .on_click({
+                                            cx.listener(move |this, _: &ClickEvent, window, cx| {
+                                                this.commit_changes(window, cx)
+                                            })
+                                        }),
+                                ),
+                        )
+                        // .when(!self.modal_open, |el| {
+                        .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+                        .child(
+                            div()
+                                .absolute()
+                                .top_1()
+                                .right_2()
+                                .opacity(0.5)
+                                .hover(|this| this.opacity(1.0))
+                                .w(expand_button_size)
+                                .child(
+                                    panel_icon_button("expand-commit-editor", IconName::Maximize)
+                                        .icon_size(IconSize::Small)
+                                        .style(ButtonStyle::Transparent)
+                                        .width(expand_button_size.into())
+                                        .on_click(cx.listener({
+                                            move |_, _, window, cx| {
+                                                window.dispatch_action(
+                                                    git::ExpandCommitEditor.boxed_clone(),
+                                                    cx,
+                                                )
+                                            }
+                                        })),
+                                ),
+                        ),
+                );
+
+            Some(footer)
+        } else {
+            None
+        }
     }
 
     fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
@@ -2105,10 +1993,10 @@ impl GitPanel {
         Some(
             h_flex()
                 .items_center()
-                .py_1p5()
+                .py_2()
                 .px(px(8.))
-                .bg(cx.theme().colors().background)
-                .border_t_1()
+                // .bg(cx.theme().colors().background)
+                // .border_t_1()
                 .border_color(cx.theme().colors().border)
                 .gap_1p5()
                 .child(
@@ -2135,11 +2023,9 @@ impl GitPanel {
                 )
                 .child(div().flex_1())
                 .child(
-                    panel_filled_button("Uncommit")
-                        .icon(IconName::Undo)
+                    panel_icon_button("undo", IconName::Undo)
                         .icon_size(IconSize::Small)
                         .icon_color(Color::Muted)
-                        .icon_position(IconPosition::Start)
                         .tooltip(Tooltip::for_action_title(
                             if self.has_staged_changes() {
                                 "git reset HEAD^ --soft"
@@ -2149,8 +2035,7 @@ impl GitPanel {
                             &git::Uncommit,
                         ))
                         .on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
-                )
-                .child(self.render_push_button(branch, cx)),
+                ),
         )
     }
 
@@ -2228,7 +2113,7 @@ impl GitPanel {
         )
     }
 
-    pub fn render_buffer_header_controls(
+    fn render_buffer_header_controls(
         &self,
         entity: &Entity<Self>,
         file: &Arc<dyn File>,
@@ -2402,26 +2287,13 @@ impl GitPanel {
         self.set_context_menu(context_menu, position, window, cx);
     }
 
-    fn panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
-        ContextMenu::build(window, cx, |context_menu, _, _| {
-            context_menu
-                .action("Stage All", StageAll.boxed_clone())
-                .action("Unstage All", UnstageAll.boxed_clone())
-                .separator()
-                .action("Open Diff", project_diff::Diff.boxed_clone())
-                .separator()
-                .action("Restore Tracked Files", RestoreTrackedFiles.boxed_clone())
-                .action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
-        })
-    }
-
     fn deploy_panel_context_menu(
         &mut self,
         position: Point<Pixels>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let context_menu = Self::panel_context_menu(window, cx);
+        let context_menu = git_panel_context_menu(window, cx);
         self.set_context_menu(context_menu, position, window, cx);
     }
 
@@ -2584,69 +2456,6 @@ impl GitPanel {
             .into_any_element()
     }
 
-    fn render_push_button(&self, branch: &Branch, cx: &Context<Self>) -> AnyElement {
-        let mut disabled = false;
-
-        // TODO: Add <origin> and <branch> argument substitutions to this
-        let button: SharedString;
-        let tooltip: SharedString;
-        let action: Option<Push>;
-        if let Some(upstream) = &branch.upstream {
-            match upstream.tracking {
-                UpstreamTracking::Gone => {
-                    button = "Republish".into();
-                    tooltip = "git push --set-upstream".into();
-                    action = Some(git::Push {
-                        options: Some(PushOptions::SetUpstream),
-                    });
-                }
-                UpstreamTracking::Tracked(tracking) => {
-                    if tracking.behind > 0 {
-                        disabled = true;
-                        button = "Push".into();
-                        tooltip = "Upstream is ahead of local branch".into();
-                        action = None;
-                    } else if tracking.ahead > 0 {
-                        button = format!("Push ({})", tracking.ahead).into();
-                        tooltip = "git push".into();
-                        action = Some(git::Push { options: None });
-                    } else {
-                        disabled = true;
-                        button = "Push".into();
-                        tooltip = "Upstream matches local branch".into();
-                        action = None;
-                    }
-                }
-            }
-        } else {
-            button = "Publish".into();
-            tooltip = "git push --set-upstream".into();
-            action = Some(git::Push {
-                options: Some(PushOptions::SetUpstream),
-            });
-        };
-
-        panel_filled_button(button)
-            .icon(IconName::ArrowUp)
-            .icon_size(IconSize::Small)
-            .icon_color(Color::Muted)
-            .icon_position(IconPosition::Start)
-            .disabled(disabled)
-            .when_some(action, |this, action| {
-                this.on_click(
-                    cx.listener(move |this, _, window, cx| this.push(&action, window, cx)),
-                )
-            })
-            .tooltip(move |window, cx| {
-                if let Some(action) = action.as_ref() {
-                    Tooltip::for_action(tooltip.clone(), action, window, cx)
-                } else {
-                    Tooltip::simple(tooltip.clone(), cx)
-                }
-            })
-            .into_any_element()
-    }
-
     fn has_write_access(&self, cx: &App) -> bool {
         !self.project.read(cx).is_read_only(cx)
     }
@@ -2718,7 +2527,6 @@ impl Render for GitPanel {
             .child(
                 v_flex()
                     .size_full()
-                    .children(self.render_panel_header(window, cx))
                     .map(|this| {
                         if has_entries {
                             this.child(self.render_entries(has_write_access, window, cx))
@@ -2726,8 +2534,8 @@ impl Render for GitPanel {
                             this.child(self.render_empty_state(cx).into_any_element())
                         }
                     })
+                    .children(self.render_footer(window, cx))
                     .children(self.render_previous_commit(cx))
-                    .child(self.render_commit_editor(window, cx))
                     .into_any_element(),
             )
             .children(self.context_menu.as_ref().map(|(menu, position, _)| {
@@ -2881,3 +2689,618 @@ impl Render for GitPanelMessageTooltip {
         }
     }
 }
+
+fn git_action_tooltip(
+    label: impl Into<SharedString>,
+    action: &dyn Action,
+    command: impl Into<SharedString>,
+    focus_handle: Option<FocusHandle>,
+    window: &mut Window,
+    cx: &mut App,
+) -> AnyView {
+    let label = label.into();
+    let command = command.into();
+
+    if let Some(handle) = focus_handle {
+        Tooltip::with_meta_in(
+            label.clone(),
+            Some(action),
+            command.clone(),
+            &handle,
+            window,
+            cx,
+        )
+    } else {
+        Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
+    }
+}
+
+#[derive(IntoElement)]
+struct SplitButton {
+    pub left: ButtonLike,
+    pub right: AnyElement,
+}
+
+impl SplitButton {
+    fn new(
+        id: impl Into<SharedString>,
+        left_label: impl Into<SharedString>,
+        ahead_count: usize,
+        behind_count: usize,
+        left_icon: Option<IconName>,
+        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        let id = id.into();
+
+        fn count(count: usize) -> impl IntoElement {
+            h_flex()
+                .ml_neg_px()
+                .h(rems(0.875))
+                .items_center()
+                .overflow_hidden()
+                .px_0p5()
+                .child(
+                    Label::new(count.to_string())
+                        .size(LabelSize::XSmall)
+                        .line_height_style(LineHeightStyle::UiLabel),
+                )
+        }
+
+        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
+
+        let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
+            format!("split-button-left-{}", id).into(),
+        ))
+        .layer(ui::ElevationIndex::ModalSurface)
+        .size(ui::ButtonSize::Compact)
+        .when(should_render_counts, |this| {
+            this.child(
+                h_flex()
+                    .ml_neg_0p5()
+                    .mr_1()
+                    .when(behind_count > 0, |this| {
+                        this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
+                            .child(count(behind_count))
+                    })
+                    .when(ahead_count > 0, |this| {
+                        this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
+                            .child(count(ahead_count))
+                    }),
+            )
+        })
+        .when_some(left_icon, |this, left_icon| {
+            this.child(
+                h_flex()
+                    .ml_neg_0p5()
+                    .mr_1()
+                    .child(Icon::new(left_icon).size(IconSize::XSmall)),
+            )
+        })
+        .child(
+            div()
+                .child(Label::new(left_label).size(LabelSize::Small))
+                .mr_0p5(),
+        )
+        .on_click(left_on_click)
+        .tooltip(tooltip);
+
+        let right =
+            render_git_action_menu(ElementId::Name(format!("split-button-right-{}", id).into()))
+                .into_any_element();
+        // .on_click(right_on_click);
+
+        Self { left, right }
+    }
+}
+
+impl RenderOnce for SplitButton {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .rounded_md()
+            .border_1()
+            .border_color(cx.theme().colors().text_muted.alpha(0.12))
+            .child(self.left)
+            .child(
+                div()
+                    .h_full()
+                    .w_px()
+                    .bg(cx.theme().colors().text_muted.alpha(0.16)),
+            )
+            .child(self.right)
+            .bg(ElevationIndex::Surface.on_elevation_bg(cx))
+            .shadow(smallvec![BoxShadow {
+                color: hsla(0.0, 0.0, 0.0, 0.16),
+                offset: point(px(0.), px(1.)),
+                blur_radius: px(0.),
+                spread_radius: px(0.),
+            }])
+    }
+}
+
+fn render_git_action_menu(id: impl Into<ElementId>) -> impl IntoElement {
+    PopoverMenu::new(id.into())
+        .trigger(
+            ui::ButtonLike::new_rounded_right("split-button-right")
+                .layer(ui::ElevationIndex::ModalSurface)
+                .size(ui::ButtonSize::None)
+                .child(
+                    div()
+                        .px_1()
+                        .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+                ),
+        )
+        .menu(move |window, cx| {
+            Some(ContextMenu::build(window, cx, |context_menu, _, _| {
+                context_menu
+                    .action("Fetch", git::Fetch.boxed_clone())
+                    .action("Pull", git::Pull.boxed_clone())
+                    .separator()
+                    .action("Push", git::Push { options: None }.boxed_clone())
+                    .action(
+                        "Force Push",
+                        git::Push {
+                            options: Some(PushOptions::Force),
+                        }
+                        .boxed_clone(),
+                    )
+            }))
+        })
+        .anchor(Corner::TopRight)
+}
+
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "git_panel")]
+pub struct PanelRepoHeader {
+    id: SharedString,
+    active_repository: SharedString,
+    branch: Option<Branch>,
+    // Getting a GitPanel in previews will be difficult.
+    //
+    // For now just take an option here, and we won't bind handlers to buttons in previews.
+    git_panel: Option<Entity<GitPanel>>,
+}
+
+impl PanelRepoHeader {
+    pub fn new(
+        id: impl Into<SharedString>,
+        active_repository: SharedString,
+        branch: Option<Branch>,
+        git_panel: Option<Entity<GitPanel>>,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            active_repository,
+            branch,
+            git_panel,
+        }
+    }
+
+    pub fn new_preview(
+        id: impl Into<SharedString>,
+        active_repository: SharedString,
+        branch: Option<Branch>,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            active_repository,
+            branch,
+            git_panel: None,
+        }
+    }
+
+    fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
+        PopoverMenu::new(id.into())
+            .trigger(
+                IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
+                    .icon_size(IconSize::Small)
+                    .icon_color(Color::Muted),
+            )
+            .menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
+            .anchor(Corner::TopRight)
+    }
+
+    fn panel_focus_handle(&self, cx: &App) -> Option<FocusHandle> {
+        if let Some(git_panel) = self.git_panel.clone() {
+            Some(git_panel.focus_handle(cx))
+        } else {
+            None
+        }
+    }
+
+    fn render_push_button(&self, id: SharedString, ahead: u32, cx: &mut App) -> SplitButton {
+        let panel_focus_handle = self.panel_focus_handle(cx);
+
+        SplitButton::new(
+            id,
+            "Push",
+            ahead as usize,
+            0,
+            None,
+            |_, _, cx| cx.dispatch_action(&git::Push { options: None }),
+            move |window, cx| {
+                git_action_tooltip(
+                    "Push committed changes to remote",
+                    &git::Push { options: None },
+                    "git push",
+                    panel_focus_handle.clone(),
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_pull_button(
+        &self,
+        id: SharedString,
+        ahead: u32,
+        behind: u32,
+        cx: &mut App,
+    ) -> SplitButton {
+        let panel_focus_handle = self.panel_focus_handle(cx);
+
+        SplitButton::new(
+            id,
+            "Pull",
+            ahead as usize,
+            behind as usize,
+            None,
+            |_, _, cx| cx.dispatch_action(&git::Pull),
+            move |window, cx| {
+                git_action_tooltip(
+                    "Pull",
+                    &git::Pull,
+                    "git pull",
+                    panel_focus_handle.clone(),
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_fetch_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
+        let panel_focus_handle = self.panel_focus_handle(cx);
+
+        SplitButton::new(
+            id,
+            "Fetch",
+            0,
+            0,
+            Some(IconName::ArrowCircle),
+            |_, _, cx| cx.dispatch_action(&git::Fetch),
+            move |window, cx| {
+                git_action_tooltip(
+                    "Fetch updates from remote",
+                    &git::Fetch,
+                    "git fetch",
+                    panel_focus_handle.clone(),
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_publish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
+        let panel_focus_handle = self.panel_focus_handle(cx);
+
+        SplitButton::new(
+            id,
+            "Publish",
+            0,
+            0,
+            Some(IconName::ArrowUpFromLine),
+            |_, _, cx| {
+                cx.dispatch_action(&git::Push {
+                    options: Some(PushOptions::SetUpstream),
+                })
+            },
+            move |window, cx| {
+                git_action_tooltip(
+                    "Publish branch to remote",
+                    &git::Push {
+                        options: Some(PushOptions::SetUpstream),
+                    },
+                    "git push --set-upstream",
+                    panel_focus_handle.clone(),
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_republish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
+        let panel_focus_handle = self.panel_focus_handle(cx);
+
+        SplitButton::new(
+            id,
+            "Republish",
+            0,
+            0,
+            Some(IconName::ArrowUpFromLine),
+            |_, _, cx| {
+                cx.dispatch_action(&git::Push {
+                    options: Some(PushOptions::SetUpstream),
+                })
+            },
+            move |window, cx| {
+                git_action_tooltip(
+                    "Re-publish branch to remote",
+                    &git::Push {
+                        options: Some(PushOptions::SetUpstream),
+                    },
+                    "git push --set-upstream",
+                    panel_focus_handle.clone(),
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_relevant_button(
+        &self,
+        id: impl Into<SharedString>,
+        branch: &Branch,
+        cx: &mut App,
+    ) -> impl IntoElement {
+        let id = id.into();
+        let upstream = branch.upstream.as_ref();
+        match upstream {
+            Some(Upstream {
+                tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
+                ..
+            }) => match (*ahead, *behind) {
+                (0, 0) => self.render_fetch_button(id, cx),
+                (ahead, 0) => self.render_push_button(id, ahead, cx),
+                (ahead, behind) => self.render_pull_button(id, ahead, behind, cx),
+            },
+            Some(Upstream {
+                tracking: UpstreamTracking::Gone,
+                ..
+            }) => self.render_republish_button(id, cx),
+            None => self.render_publish_button(id, cx),
+        }
+    }
+}
+
+impl RenderOnce for PanelRepoHeader {
+    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+        let active_repo = self.active_repository.clone();
+        let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
+
+        let repo_selector = if let Some(panel) = self.git_panel.clone() {
+            RepositorySelectorPopoverMenu::new(
+                panel.read(cx).repository_selector.clone(),
+                Button::new("repo-selector", active_repo.clone())
+                    .style(ButtonStyle::Transparent)
+                    .size(ButtonSize::None)
+                    .label_size(LabelSize::Small)
+                    .color(Color::Muted),
+                Tooltip::text("Choose a repository"),
+            )
+            .into_any_element()
+        } else {
+            Button::new("repo-selector", active_repo.clone())
+                .style(ButtonStyle::Transparent)
+                .size(ButtonSize::None)
+                .label_size(LabelSize::Small)
+                .color(Color::Muted)
+                .into_any_element()
+        };
+
+        let branch = self.branch.clone();
+        let branch_name = branch
+            .as_ref()
+            .map_or("<no branch>".into(), |branch| branch.name.clone());
+
+        let branch_selector = Button::new("branch-selector", branch_name)
+            .style(ButtonStyle::Transparent)
+            .size(ButtonSize::None)
+            .label_size(LabelSize::Small)
+            .tooltip(Tooltip::for_action_title(
+                "Switch Branch",
+                &zed_actions::git::Branch,
+            ))
+            .on_click(|_, window, cx| {
+                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+            });
+
+        let spinner = self
+            .git_panel
+            .as_ref()
+            .and_then(|git_panel| git_panel.read(cx).render_spinner());
+
+        h_flex()
+            .w_full()
+            .px_2()
+            .h(px(36.))
+            .items_center()
+            .justify_between()
+            .child(
+                h_flex()
+                    .relative()
+                    .items_center()
+                    .gap_0p5()
+                    .child(
+                        div()
+                            // .when(repo_or_branch_has_uppercase, |this| {
+                            //     this.relative().pt(px(2.))
+                            // })
+                            .child(
+                                Icon::new(IconName::GitBranchSmall)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            ),
+                    )
+                    .child(
+                        h_flex()
+                            .gap_0p5()
+                            .child(repo_selector)
+                            .child(
+                                div()
+                                    .text_color(cx.theme().colors().text_muted)
+                                    .text_sm()
+                                    .child("/"),
+                            )
+                            .child(branch_selector),
+                    ),
+            )
+            .child(
+                h_flex()
+                    .gap_1()
+                    .children(spinner)
+                    .child(self.render_overflow_menu(overflow_menu_id))
+                    .when_some(branch, |this, branch| {
+                        let button = self.render_relevant_button(self.id.clone(), &branch, cx);
+                        this.child(button)
+                    }),
+            )
+    }
+}
+
+impl ComponentPreview for PanelRepoHeader {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
+        let unknown_upstream = None;
+        let no_remote_upstream = Some(UpstreamTracking::Gone);
+        let ahead_of_upstream = Some(
+            UpstreamTrackingStatus {
+                ahead: 2,
+                behind: 0,
+            }
+            .into(),
+        );
+        let behind_upstream = Some(
+            UpstreamTrackingStatus {
+                ahead: 0,
+                behind: 2,
+            }
+            .into(),
+        );
+        let ahead_and_behind_upstream = Some(
+            UpstreamTrackingStatus {
+                ahead: 3,
+                behind: 1,
+            }
+            .into(),
+        );
+
+        let not_ahead_or_behind_upstream = Some(
+            UpstreamTrackingStatus {
+                ahead: 0,
+                behind: 0,
+            }
+            .into(),
+        );
+
+        fn branch(upstream: Option<UpstreamTracking>) -> Branch {
+            Branch {
+                is_head: true,
+                name: "some-branch".into(),
+                upstream: upstream.map(|tracking| Upstream {
+                    ref_name: "origin/some-branch".into(),
+                    tracking,
+                }),
+                most_recent_commit: Some(CommitSummary {
+                    sha: "abc123".into(),
+                    subject: "Modify stuff".into(),
+                    commit_timestamp: 1710932954,
+                }),
+            }
+        }
+
+        fn active_repository(id: usize) -> SharedString {
+            format!("repo-{}", id).into()
+        }
+
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Action Button States",
+                vec![
+                    single_example(
+                        "No Branch",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "no-branch",
+                                active_repository(1).clone(),
+                                None,
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Remote status unknown",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "unknown-upstream",
+                                active_repository(2).clone(),
+                                Some(branch(unknown_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "No Remote Upstream",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "no-remote-upstream",
+                                active_repository(3).clone(),
+                                Some(branch(no_remote_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Not Ahead or Behind",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "not-ahead-or-behind",
+                                active_repository(4).clone(),
+                                Some(branch(not_ahead_or_behind_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Behind remote",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "behind-remote",
+                                active_repository(5).clone(),
+                                Some(branch(behind_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Ahead of remote",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "ahead-of-remote",
+                                active_repository(6).clone(),
+                                Some(branch(ahead_of_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Ahead and behind remote",
+                        div()
+                            .w(px(180.))
+                            .child(PanelRepoHeader::new_preview(
+                                "ahead-and-behind",
+                                active_repository(7).clone(),
+                                Some(branch(ahead_and_behind_upstream)),
+                            ))
+                            .into_any_element(),
+                    ),
+                ],
+            )
+            .vertical()])
+            .into_any_element()
+    }
+}

crates/panel/src/panel.rs 🔗

@@ -79,7 +79,7 @@ pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
         .bg(cx.theme().colors().editor_background)
 }
 
-pub fn panel_editor_style(monospace: bool, window: &mut Window, cx: &mut App) -> EditorStyle {
+pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorStyle {
     let settings = ThemeSettings::get_global(cx);
 
     let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());

crates/ui/src/components/avatar.rs 🔗

@@ -220,7 +220,7 @@ impl RenderOnce for AvatarAvailabilityIndicator {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Avatar {
-    fn preview(_window: &mut Window, cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
         let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
 
         v_flex()

crates/ui/src/components/button/button.rs 🔗

@@ -458,7 +458,7 @@ impl RenderOnce for Button {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Button {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![

crates/ui/src/components/button/icon_button.rs 🔗

@@ -202,7 +202,7 @@ impl RenderOnce for IconButton {
 }
 
 impl ComponentPreview for IconButton {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -144,7 +144,7 @@ impl RenderOnce for ToggleButton {
 }
 
 impl ComponentPreview for ToggleButton {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![

crates/ui/src/components/content_group.rs 🔗

@@ -90,7 +90,7 @@ impl RenderOnce for ContentGroup {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for ContentGroup {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         example_group(vec![
             single_example(
                 "Default",

crates/ui/src/components/facepile.rs 🔗

@@ -61,7 +61,7 @@ impl RenderOnce for Facepile {
 }
 
 impl ComponentPreview for Facepile {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         let faces: [&'static str; 6] = [
             "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
             "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",

crates/ui/src/components/icon.rs 🔗

@@ -218,6 +218,7 @@ pub enum IconName {
     Github,
     Globe,
     GitBranch,
+    GitBranchSmall,
     Hash,
     HistoryRerun,
     Indicator,
@@ -492,7 +493,7 @@ impl RenderOnce for IconWithIndicator {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Icon {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![

crates/ui/src/components/icon/decorated_icon.rs 🔗

@@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for DecoratedIcon {
-    fn preview(_window: &mut Window, cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
         let decoration_x = IconDecoration::new(
             IconDecorationKind::X,
             cx.theme().colors().surface_background,

crates/ui/src/components/keybinding_hint.rs 🔗

@@ -207,7 +207,7 @@ impl RenderOnce for KeybindingHint {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for KeybindingHint {
-    fn preview(window: &mut Window, cx: &App) -> AnyElement {
+    fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
         let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
         let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
             .unwrap_or(KeyBinding::new(enter_fallback, cx));

crates/ui/src/components/label/label.rs 🔗

@@ -199,7 +199,7 @@ mod label_preview {
 
     // View this component preview using `workspace: open component-preview`
     impl ComponentPreview for Label {
-        fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
             v_flex()
                 .gap_6()
                 .children(vec![

crates/ui/src/components/tab.rs 🔗

@@ -173,7 +173,7 @@ impl RenderOnce for Tab {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Tab {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![example_group_with_title(

crates/ui/src/components/table.rs 🔗

@@ -153,7 +153,7 @@ where
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Table {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![

crates/ui/src/components/toggle.rs 🔗

@@ -510,7 +510,7 @@ impl RenderOnce for SwitchWithLabel {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Checkbox {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![
@@ -595,7 +595,7 @@ impl ComponentPreview for Checkbox {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Switch {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![
@@ -658,7 +658,7 @@ impl ComponentPreview for Switch {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for CheckboxWithLabel {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_6()
             .children(vec![example_group_with_title(

crates/ui/src/components/tooltip.rs 🔗

@@ -224,7 +224,7 @@ impl Render for LinkPreview {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Tooltip {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         example_group(vec![single_example(
             "Text only",
             Button::new("delete-example", "Delete")

crates/ui/src/styles/typography.rs 🔗

@@ -235,7 +235,7 @@ impl Headline {
 
 // View this component preview using `workspace: open component-preview`
 impl ComponentPreview for Headline {
-    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
         v_flex()
             .gap_1()
             .children(vec![