git_panel.rs

   1use crate::branch_picker::{self};
   2use crate::git_panel_settings::StatusStyle;
   3use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
   4use crate::{
   5    git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
   6};
   7use crate::{picker_prompt, project_diff, ProjectDiff};
   8use db::kvp::KEY_VALUE_STORE;
   9use editor::commit_tooltip::CommitTooltip;
  10
  11use editor::{
  12    scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
  13    ShowScrollbar,
  14};
  15use futures::StreamExt as _;
  16use git::repository::{
  17    Branch, CommitDetails, CommitSummary, PushOptions, Remote, RemoteCommandOutput, ResetMode,
  18    Upstream, UpstreamTracking, UpstreamTrackingStatus,
  19};
  20use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
  21use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
  22use gpui::*;
  23use itertools::Itertools;
  24use language::{Buffer, File};
  25use language_model::{
  26    LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
  27};
  28use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  29use multi_buffer::ExcerptInfo;
  30use panel::{
  31    panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, PanelHeader,
  32};
  33use project::{
  34    git::{GitEvent, Repository},
  35    Fs, Project, ProjectPath,
  36};
  37use serde::{Deserialize, Serialize};
  38use settings::Settings as _;
  39use smallvec::smallvec;
  40use std::cell::RefCell;
  41use std::future::Future;
  42use std::rc::Rc;
  43use std::{collections::HashSet, sync::Arc, time::Duration, usize};
  44use strum::{IntoEnumIterator, VariantNames};
  45use time::OffsetDateTime;
  46use ui::{
  47    prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar,
  48    ScrollbarState, Tooltip,
  49};
  50use util::{maybe, post_inc, ResultExt, TryFutureExt};
  51
  52use workspace::{
  53    dock::{DockPosition, Panel, PanelEvent},
  54    notifications::{DetachAndPromptErr, NotificationId},
  55    Toast, Workspace,
  56};
  57
  58actions!(
  59    git_panel,
  60    [
  61        Close,
  62        ToggleFocus,
  63        OpenMenu,
  64        FocusEditor,
  65        FocusChanges,
  66        ToggleFillCoAuthors,
  67    ]
  68);
  69
  70fn prompt<T>(msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App) -> Task<Result<T>>
  71where
  72    T: IntoEnumIterator + VariantNames + 'static,
  73{
  74    let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx);
  75    cx.spawn(|_| async move { Ok(T::iter().nth(rx.await?).unwrap()) })
  76}
  77
  78#[derive(strum::EnumIter, strum::VariantNames)]
  79#[strum(serialize_all = "title_case")]
  80enum TrashCancel {
  81    Trash,
  82    Cancel,
  83}
  84
  85fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
  86    ContextMenu::build(window, cx, |context_menu, _, _| {
  87        context_menu
  88            .action("Stage All", StageAll.boxed_clone())
  89            .action("Unstage All", UnstageAll.boxed_clone())
  90            .separator()
  91            .action("Open Diff", project_diff::Diff.boxed_clone())
  92            .separator()
  93            .action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
  94            .action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
  95    })
  96}
  97
  98const GIT_PANEL_KEY: &str = "GitPanel";
  99
 100const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 101
 102pub fn init(cx: &mut App) {
 103    cx.observe_new(
 104        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 105            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 106                workspace.toggle_panel_focus::<GitPanel>(window, cx);
 107            });
 108        },
 109    )
 110    .detach();
 111}
 112
 113#[derive(Debug, Clone)]
 114pub enum Event {
 115    Focus,
 116}
 117
 118#[derive(Serialize, Deserialize)]
 119struct SerializedGitPanel {
 120    width: Option<Pixels>,
 121}
 122
 123#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 124enum Section {
 125    Conflict,
 126    Tracked,
 127    New,
 128}
 129
 130#[derive(Debug, PartialEq, Eq, Clone)]
 131struct GitHeaderEntry {
 132    header: Section,
 133}
 134
 135impl GitHeaderEntry {
 136    pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool {
 137        let this = &self.header;
 138        let status = status_entry.status;
 139        match this {
 140            Section::Conflict => repo.has_conflict(&status_entry.repo_path),
 141            Section::Tracked => !status.is_created(),
 142            Section::New => status.is_created(),
 143        }
 144    }
 145    pub fn title(&self) -> &'static str {
 146        match self.header {
 147            Section::Conflict => "Conflicts",
 148            Section::Tracked => "Tracked",
 149            Section::New => "Untracked",
 150        }
 151    }
 152}
 153
 154#[derive(Debug, PartialEq, Eq, Clone)]
 155enum GitListEntry {
 156    GitStatusEntry(GitStatusEntry),
 157    Header(GitHeaderEntry),
 158}
 159
 160impl GitListEntry {
 161    fn status_entry(&self) -> Option<&GitStatusEntry> {
 162        match self {
 163            GitListEntry::GitStatusEntry(entry) => Some(entry),
 164            _ => None,
 165        }
 166    }
 167}
 168
 169#[derive(Debug, PartialEq, Eq, Clone)]
 170pub struct GitStatusEntry {
 171    pub(crate) repo_path: RepoPath,
 172    pub(crate) status: FileStatus,
 173    pub(crate) is_staged: Option<bool>,
 174}
 175
 176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 177enum TargetStatus {
 178    Staged,
 179    Unstaged,
 180    Reverted,
 181    Unchanged,
 182}
 183
 184struct PendingOperation {
 185    finished: bool,
 186    target_status: TargetStatus,
 187    repo_paths: HashSet<RepoPath>,
 188    op_id: usize,
 189}
 190
 191type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
 192
 193pub struct GitPanel {
 194    remote_operation_id: u32,
 195    pending_remote_operations: RemoteOperations,
 196    pub(crate) active_repository: Option<Entity<Repository>>,
 197    pub(crate) commit_editor: Entity<Editor>,
 198    conflicted_count: usize,
 199    conflicted_staged_count: usize,
 200    current_modifiers: Modifiers,
 201    add_coauthors: bool,
 202    generate_commit_message_task: Option<Task<Option<()>>>,
 203    entries: Vec<GitListEntry>,
 204    focus_handle: FocusHandle,
 205    fs: Arc<dyn Fs>,
 206    hide_scrollbar_task: Option<Task<()>>,
 207    new_count: usize,
 208    new_staged_count: usize,
 209    pending: Vec<PendingOperation>,
 210    pending_commit: Option<Task<()>>,
 211    pending_serialization: Task<Option<()>>,
 212    pub(crate) project: Entity<Project>,
 213    scroll_handle: UniformListScrollHandle,
 214    scrollbar_state: ScrollbarState,
 215    selected_entry: Option<usize>,
 216    marked_entries: Vec<usize>,
 217    show_scrollbar: bool,
 218    tracked_count: usize,
 219    tracked_staged_count: usize,
 220    update_visible_entries_task: Task<()>,
 221    width: Option<Pixels>,
 222    workspace: WeakEntity<Workspace>,
 223    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 224    modal_open: bool,
 225}
 226
 227struct RemoteOperationGuard {
 228    id: u32,
 229    pending_remote_operations: RemoteOperations,
 230}
 231
 232impl Drop for RemoteOperationGuard {
 233    fn drop(&mut self) {
 234        self.pending_remote_operations.borrow_mut().remove(&self.id);
 235    }
 236}
 237
 238pub(crate) fn commit_message_editor(
 239    commit_message_buffer: Entity<Buffer>,
 240    placeholder: Option<&str>,
 241    project: Entity<Project>,
 242    in_panel: bool,
 243    window: &mut Window,
 244    cx: &mut Context<'_, Editor>,
 245) -> Editor {
 246    let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
 247    let max_lines = if in_panel { 6 } else { 18 };
 248    let mut commit_editor = Editor::new(
 249        EditorMode::AutoHeight { max_lines },
 250        buffer,
 251        None,
 252        false,
 253        window,
 254        cx,
 255    );
 256    commit_editor.set_collaboration_hub(Box::new(project));
 257    commit_editor.set_use_autoclose(false);
 258    commit_editor.set_show_gutter(false, cx);
 259    commit_editor.set_show_wrap_guides(false, cx);
 260    commit_editor.set_show_indent_guides(false, cx);
 261    let placeholder = placeholder.unwrap_or("Enter commit message");
 262    commit_editor.set_placeholder_text(placeholder, cx);
 263    commit_editor
 264}
 265
 266impl GitPanel {
 267    pub fn new(
 268        workspace: &mut Workspace,
 269        window: &mut Window,
 270        cx: &mut Context<Workspace>,
 271    ) -> Entity<Self> {
 272        let fs = workspace.app_state().fs.clone();
 273        let project = workspace.project().clone();
 274        let git_store = project.read(cx).git_store().clone();
 275        let active_repository = project.read(cx).active_repository(cx);
 276        let workspace = cx.entity().downgrade();
 277
 278        cx.new(|cx| {
 279            let focus_handle = cx.focus_handle();
 280            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
 281            cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
 282                this.hide_scrollbar(window, cx);
 283            })
 284            .detach();
 285
 286            // just to let us render a placeholder editor.
 287            // Once the active git repo is set, this buffer will be replaced.
 288            let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
 289            let commit_editor = cx.new(|cx| {
 290                commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
 291            });
 292
 293            commit_editor.update(cx, |editor, cx| {
 294                editor.clear(window, cx);
 295            });
 296
 297            let scroll_handle = UniformListScrollHandle::new();
 298
 299            cx.subscribe_in(
 300                &git_store,
 301                window,
 302                move |this, git_store, event, window, cx| match event {
 303                    GitEvent::FileSystemUpdated => {
 304                        this.schedule_update(false, window, cx);
 305                    }
 306                    GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
 307                        this.active_repository = git_store.read(cx).active_repository();
 308                        this.schedule_update(true, window, cx);
 309                    }
 310                    GitEvent::IndexWriteError(error) => {
 311                        this.workspace
 312                            .update(cx, |workspace, cx| {
 313                                workspace.show_error(error, cx);
 314                            })
 315                            .ok();
 316                    }
 317                },
 318            )
 319            .detach();
 320
 321            let scrollbar_state =
 322                ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
 323
 324            let mut git_panel = Self {
 325                pending_remote_operations: Default::default(),
 326                remote_operation_id: 0,
 327                active_repository,
 328                commit_editor,
 329                conflicted_count: 0,
 330                conflicted_staged_count: 0,
 331                current_modifiers: window.modifiers(),
 332                add_coauthors: true,
 333                generate_commit_message_task: None,
 334                entries: Vec::new(),
 335                focus_handle: cx.focus_handle(),
 336                fs,
 337                hide_scrollbar_task: None,
 338                new_count: 0,
 339                new_staged_count: 0,
 340                pending: Vec::new(),
 341                pending_commit: None,
 342                pending_serialization: Task::ready(None),
 343                project,
 344                scroll_handle,
 345                scrollbar_state,
 346                selected_entry: None,
 347                marked_entries: Vec::new(),
 348                show_scrollbar: false,
 349                tracked_count: 0,
 350                tracked_staged_count: 0,
 351                update_visible_entries_task: Task::ready(()),
 352                width: Some(px(360.)),
 353                context_menu: None,
 354                workspace,
 355                modal_open: false,
 356            };
 357            git_panel.schedule_update(false, window, cx);
 358            git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
 359            git_panel
 360        })
 361    }
 362
 363    pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
 364        fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
 365        where
 366            F: Fn(usize) -> std::cmp::Ordering,
 367        {
 368            while low < high {
 369                let mid = low + (high - low) / 2;
 370                match is_target(mid) {
 371                    std::cmp::Ordering::Equal => return Some(mid),
 372                    std::cmp::Ordering::Less => low = mid + 1,
 373                    std::cmp::Ordering::Greater => high = mid,
 374                }
 375            }
 376            None
 377        }
 378        if self.conflicted_count > 0 {
 379            let conflicted_start = 1;
 380            if let Some(ix) = binary_search(
 381                conflicted_start,
 382                conflicted_start + self.conflicted_count,
 383                |ix| {
 384                    self.entries[ix]
 385                        .status_entry()
 386                        .unwrap()
 387                        .repo_path
 388                        .cmp(&path)
 389                },
 390            ) {
 391                return Some(ix);
 392            }
 393        }
 394        if self.tracked_count > 0 {
 395            let tracked_start = if self.conflicted_count > 0 {
 396                1 + self.conflicted_count
 397            } else {
 398                0
 399            } + 1;
 400            if let Some(ix) =
 401                binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
 402                    self.entries[ix]
 403                        .status_entry()
 404                        .unwrap()
 405                        .repo_path
 406                        .cmp(&path)
 407                })
 408            {
 409                return Some(ix);
 410            }
 411        }
 412        if self.new_count > 0 {
 413            let untracked_start = if self.conflicted_count > 0 {
 414                1 + self.conflicted_count
 415            } else {
 416                0
 417            } + if self.tracked_count > 0 {
 418                1 + self.tracked_count
 419            } else {
 420                0
 421            } + 1;
 422            if let Some(ix) =
 423                binary_search(untracked_start, untracked_start + self.new_count, |ix| {
 424                    self.entries[ix]
 425                        .status_entry()
 426                        .unwrap()
 427                        .repo_path
 428                        .cmp(&path)
 429                })
 430            {
 431                return Some(ix);
 432            }
 433        }
 434        None
 435    }
 436
 437    pub fn select_entry_by_path(
 438        &mut self,
 439        path: ProjectPath,
 440        _: &mut Window,
 441        cx: &mut Context<Self>,
 442    ) {
 443        let Some(git_repo) = self.active_repository.as_ref() else {
 444            return;
 445        };
 446        let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
 447            return;
 448        };
 449        let Some(ix) = self.entry_by_path(&repo_path) else {
 450            return;
 451        };
 452        self.selected_entry = Some(ix);
 453        cx.notify();
 454    }
 455
 456    fn start_remote_operation(&mut self) -> RemoteOperationGuard {
 457        let id = post_inc(&mut self.remote_operation_id);
 458        self.pending_remote_operations.borrow_mut().insert(id);
 459
 460        RemoteOperationGuard {
 461            id,
 462            pending_remote_operations: self.pending_remote_operations.clone(),
 463        }
 464    }
 465
 466    fn serialize(&mut self, cx: &mut Context<Self>) {
 467        let width = self.width;
 468        self.pending_serialization = cx.background_spawn(
 469            async move {
 470                KEY_VALUE_STORE
 471                    .write_kvp(
 472                        GIT_PANEL_KEY.into(),
 473                        serde_json::to_string(&SerializedGitPanel { width })?,
 474                    )
 475                    .await?;
 476                anyhow::Ok(())
 477            }
 478            .log_err(),
 479        );
 480    }
 481
 482    pub(crate) fn set_modal_open(&mut self, open: bool, cx: &mut Context<Self>) {
 483        self.modal_open = open;
 484        cx.notify();
 485    }
 486
 487    fn dispatch_context(&self, window: &mut Window, cx: &Context<Self>) -> KeyContext {
 488        let mut dispatch_context = KeyContext::new_with_defaults();
 489        dispatch_context.add("GitPanel");
 490
 491        if self.is_focused(window, cx) {
 492            dispatch_context.add("menu");
 493            dispatch_context.add("ChangesList");
 494        }
 495
 496        if self.commit_editor.read(cx).is_focused(window) {
 497            dispatch_context.add("CommitEditor");
 498        }
 499
 500        dispatch_context
 501    }
 502
 503    fn is_focused(&self, window: &Window, cx: &Context<Self>) -> bool {
 504        window
 505            .focused(cx)
 506            .map_or(false, |focused| self.focus_handle == focused)
 507    }
 508
 509    fn close_panel(&mut self, _: &Close, _window: &mut Window, cx: &mut Context<Self>) {
 510        cx.emit(PanelEvent::Close);
 511    }
 512
 513    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 514        if !self.focus_handle.contains_focused(window, cx) {
 515            cx.emit(Event::Focus);
 516        }
 517    }
 518
 519    fn show_scrollbar(&self, cx: &mut Context<Self>) -> ShowScrollbar {
 520        GitPanelSettings::get_global(cx)
 521            .scrollbar
 522            .show
 523            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
 524    }
 525
 526    fn should_show_scrollbar(&self, cx: &mut Context<Self>) -> bool {
 527        let show = self.show_scrollbar(cx);
 528        match show {
 529            ShowScrollbar::Auto => true,
 530            ShowScrollbar::System => true,
 531            ShowScrollbar::Always => true,
 532            ShowScrollbar::Never => false,
 533        }
 534    }
 535
 536    fn should_autohide_scrollbar(&self, cx: &mut Context<Self>) -> bool {
 537        let show = self.show_scrollbar(cx);
 538        match show {
 539            ShowScrollbar::Auto => true,
 540            ShowScrollbar::System => cx
 541                .try_global::<ScrollbarAutoHide>()
 542                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 543            ShowScrollbar::Always => false,
 544            ShowScrollbar::Never => true,
 545        }
 546    }
 547
 548    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 549        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 550        if !self.should_autohide_scrollbar(cx) {
 551            return;
 552        }
 553        self.hide_scrollbar_task = Some(cx.spawn_in(window, |panel, mut cx| async move {
 554            cx.background_executor()
 555                .timer(SCROLLBAR_SHOW_INTERVAL)
 556                .await;
 557            panel
 558                .update(&mut cx, |panel, cx| {
 559                    panel.show_scrollbar = false;
 560                    cx.notify();
 561                })
 562                .log_err();
 563        }))
 564    }
 565
 566    fn handle_modifiers_changed(
 567        &mut self,
 568        event: &ModifiersChangedEvent,
 569        _: &mut Window,
 570        cx: &mut Context<Self>,
 571    ) {
 572        self.current_modifiers = event.modifiers;
 573        cx.notify();
 574    }
 575
 576    fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
 577        if let Some(selected_entry) = self.selected_entry {
 578            self.scroll_handle
 579                .scroll_to_item(selected_entry, ScrollStrategy::Center);
 580        }
 581
 582        cx.notify();
 583    }
 584
 585    fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
 586        if !self.entries.is_empty() {
 587            self.selected_entry = Some(1);
 588            self.scroll_to_selected_entry(cx);
 589        }
 590    }
 591
 592    fn select_previous(
 593        &mut self,
 594        _: &SelectPrevious,
 595        _window: &mut Window,
 596        cx: &mut Context<Self>,
 597    ) {
 598        let item_count = self.entries.len();
 599        if item_count == 0 {
 600            return;
 601        }
 602
 603        if let Some(selected_entry) = self.selected_entry {
 604            let new_selected_entry = if selected_entry > 0 {
 605                selected_entry - 1
 606            } else {
 607                selected_entry
 608            };
 609
 610            if matches!(
 611                self.entries.get(new_selected_entry),
 612                Some(GitListEntry::Header(..))
 613            ) {
 614                if new_selected_entry > 0 {
 615                    self.selected_entry = Some(new_selected_entry - 1)
 616                }
 617            } else {
 618                self.selected_entry = Some(new_selected_entry);
 619            }
 620
 621            self.scroll_to_selected_entry(cx);
 622        }
 623
 624        cx.notify();
 625    }
 626
 627    fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
 628        let item_count = self.entries.len();
 629        if item_count == 0 {
 630            return;
 631        }
 632
 633        if let Some(selected_entry) = self.selected_entry {
 634            let new_selected_entry = if selected_entry < item_count - 1 {
 635                selected_entry + 1
 636            } else {
 637                selected_entry
 638            };
 639            if matches!(
 640                self.entries.get(new_selected_entry),
 641                Some(GitListEntry::Header(..))
 642            ) {
 643                self.selected_entry = Some(new_selected_entry + 1);
 644            } else {
 645                self.selected_entry = Some(new_selected_entry);
 646            }
 647
 648            self.scroll_to_selected_entry(cx);
 649        }
 650
 651        cx.notify();
 652    }
 653
 654    fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
 655        if self.entries.last().is_some() {
 656            self.selected_entry = Some(self.entries.len() - 1);
 657            self.scroll_to_selected_entry(cx);
 658        }
 659    }
 660
 661    pub(crate) fn editor_focus_handle(&self, cx: &mut Context<Self>) -> FocusHandle {
 662        self.commit_editor.focus_handle(cx).clone()
 663    }
 664
 665    fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
 666        self.commit_editor.update(cx, |editor, cx| {
 667            window.focus(&editor.focus_handle(cx));
 668        });
 669        cx.notify();
 670    }
 671
 672    fn select_first_entry_if_none(&mut self, cx: &mut Context<Self>) {
 673        let have_entries = self
 674            .active_repository
 675            .as_ref()
 676            .map_or(false, |active_repository| {
 677                active_repository.read(cx).entry_count() > 0
 678            });
 679        if have_entries && self.selected_entry.is_none() {
 680            self.selected_entry = Some(1);
 681            self.scroll_to_selected_entry(cx);
 682            cx.notify();
 683        }
 684    }
 685
 686    fn focus_changes_list(
 687        &mut self,
 688        _: &FocusChanges,
 689        window: &mut Window,
 690        cx: &mut Context<Self>,
 691    ) {
 692        self.select_first_entry_if_none(cx);
 693
 694        cx.focus_self(window);
 695        cx.notify();
 696    }
 697
 698    fn get_selected_entry(&self) -> Option<&GitListEntry> {
 699        self.selected_entry.and_then(|i| self.entries.get(i))
 700    }
 701
 702    fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 703        maybe!({
 704            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
 705            let workspace = self.workspace.upgrade()?;
 706            let git_repo = self.active_repository.as_ref()?;
 707
 708            if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
 709                if let Some(project_path) = project_diff.read(cx).active_path(cx) {
 710                    if Some(&entry.repo_path)
 711                        == git_repo
 712                            .read(cx)
 713                            .project_path_to_repo_path(&project_path)
 714                            .as_ref()
 715                    {
 716                        project_diff.focus_handle(cx).focus(window);
 717                        return None;
 718                    }
 719                }
 720            };
 721
 722            self.workspace
 723                .update(cx, |workspace, cx| {
 724                    ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
 725                })
 726                .ok();
 727            self.focus_handle.focus(window);
 728
 729            Some(())
 730        });
 731    }
 732
 733    fn open_file(
 734        &mut self,
 735        _: &menu::SecondaryConfirm,
 736        window: &mut Window,
 737        cx: &mut Context<Self>,
 738    ) {
 739        maybe!({
 740            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
 741            let active_repo = self.active_repository.as_ref()?;
 742            let path = active_repo
 743                .read(cx)
 744                .repo_path_to_project_path(&entry.repo_path)?;
 745            if entry.status.is_deleted() {
 746                return None;
 747            }
 748
 749            self.workspace
 750                .update(cx, |workspace, cx| {
 751                    workspace
 752                        .open_path_preview(path, None, false, false, true, window, cx)
 753                        .detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
 754                            Some(format!("{e}"))
 755                        });
 756                })
 757                .ok()
 758        });
 759    }
 760
 761    fn revert_selected(
 762        &mut self,
 763        _: &git::RestoreFile,
 764        window: &mut Window,
 765        cx: &mut Context<Self>,
 766    ) {
 767        maybe!({
 768            let list_entry = self.entries.get(self.selected_entry?)?.clone();
 769            let entry = list_entry.status_entry()?;
 770            self.revert_entry(&entry, window, cx);
 771            Some(())
 772        });
 773    }
 774
 775    fn revert_entry(
 776        &mut self,
 777        entry: &GitStatusEntry,
 778        window: &mut Window,
 779        cx: &mut Context<Self>,
 780    ) {
 781        maybe!({
 782            let active_repo = self.active_repository.clone()?;
 783            let path = active_repo
 784                .read(cx)
 785                .repo_path_to_project_path(&entry.repo_path)?;
 786            let workspace = self.workspace.clone();
 787
 788            if entry.status.is_staged() != Some(false) {
 789                self.perform_stage(false, vec![entry.repo_path.clone()], cx);
 790            }
 791            let filename = path.path.file_name()?.to_string_lossy();
 792
 793            if !entry.status.is_created() {
 794                self.perform_checkout(vec![entry.repo_path.clone()], cx);
 795            } else {
 796                let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
 797                cx.spawn_in(window, |_, mut cx| async move {
 798                    match prompt.await? {
 799                        TrashCancel::Trash => {}
 800                        TrashCancel::Cancel => return Ok(()),
 801                    }
 802                    let task = workspace.update(&mut cx, |workspace, cx| {
 803                        workspace
 804                            .project()
 805                            .update(cx, |project, cx| project.delete_file(path, true, cx))
 806                    })?;
 807                    if let Some(task) = task {
 808                        task.await?;
 809                    }
 810                    Ok(())
 811                })
 812                .detach_and_prompt_err(
 813                    "Failed to trash file",
 814                    window,
 815                    cx,
 816                    |e, _, _| Some(format!("{e}")),
 817                );
 818            }
 819            Some(())
 820        });
 821    }
 822
 823    fn perform_checkout(&mut self, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
 824        let workspace = self.workspace.clone();
 825        let Some(active_repository) = self.active_repository.clone() else {
 826            return;
 827        };
 828
 829        let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
 830        self.pending.push(PendingOperation {
 831            op_id,
 832            target_status: TargetStatus::Reverted,
 833            repo_paths: repo_paths.iter().cloned().collect(),
 834            finished: false,
 835        });
 836        self.update_visible_entries(cx);
 837        let task = cx.spawn(|_, mut cx| async move {
 838            let tasks: Vec<_> = workspace.update(&mut cx, |workspace, cx| {
 839                workspace.project().update(cx, |project, cx| {
 840                    repo_paths
 841                        .iter()
 842                        .filter_map(|repo_path| {
 843                            let path = active_repository
 844                                .read(cx)
 845                                .repo_path_to_project_path(&repo_path)?;
 846                            Some(project.open_buffer(path, cx))
 847                        })
 848                        .collect()
 849                })
 850            })?;
 851
 852            let buffers = futures::future::join_all(tasks).await;
 853
 854            active_repository
 855                .update(&mut cx, |repo, _| repo.checkout_files("HEAD", repo_paths))?
 856                .await??;
 857
 858            let tasks: Vec<_> = cx.update(|cx| {
 859                buffers
 860                    .iter()
 861                    .filter_map(|buffer| {
 862                        buffer.as_ref().ok()?.update(cx, |buffer, cx| {
 863                            buffer.is_dirty().then(|| buffer.reload(cx))
 864                        })
 865                    })
 866                    .collect()
 867            })?;
 868
 869            futures::future::join_all(tasks).await;
 870
 871            Ok(())
 872        });
 873
 874        cx.spawn(|this, mut cx| async move {
 875            let result = task.await;
 876
 877            this.update(&mut cx, |this, cx| {
 878                for pending in this.pending.iter_mut() {
 879                    if pending.op_id == op_id {
 880                        pending.finished = true;
 881                        if result.is_err() {
 882                            pending.target_status = TargetStatus::Unchanged;
 883                            this.update_visible_entries(cx);
 884                        }
 885                        break;
 886                    }
 887                }
 888                result
 889                    .map_err(|e| {
 890                        this.show_err_toast(e, cx);
 891                    })
 892                    .ok();
 893            })
 894            .ok();
 895        })
 896        .detach();
 897    }
 898
 899    fn restore_tracked_files(
 900        &mut self,
 901        _: &RestoreTrackedFiles,
 902        window: &mut Window,
 903        cx: &mut Context<Self>,
 904    ) {
 905        let entries = self
 906            .entries
 907            .iter()
 908            .filter_map(|entry| entry.status_entry().cloned())
 909            .filter(|status_entry| !status_entry.status.is_created())
 910            .collect::<Vec<_>>();
 911
 912        match entries.len() {
 913            0 => return,
 914            1 => return self.revert_entry(&entries[0], window, cx),
 915            _ => {}
 916        }
 917        let mut details = entries
 918            .iter()
 919            .filter_map(|entry| entry.repo_path.0.file_name())
 920            .map(|filename| filename.to_string_lossy())
 921            .take(5)
 922            .join("\n");
 923        if entries.len() > 5 {
 924            details.push_str(&format!("\nand {} more…", entries.len() - 5))
 925        }
 926
 927        #[derive(strum::EnumIter, strum::VariantNames)]
 928        #[strum(serialize_all = "title_case")]
 929        enum RestoreCancel {
 930            RestoreTrackedFiles,
 931            Cancel,
 932        }
 933        let prompt = prompt(
 934            "Discard changes to these files?",
 935            Some(&details),
 936            window,
 937            cx,
 938        );
 939        cx.spawn(|this, mut cx| async move {
 940            match prompt.await {
 941                Ok(RestoreCancel::RestoreTrackedFiles) => {
 942                    this.update(&mut cx, |this, cx| {
 943                        let repo_paths = entries.into_iter().map(|entry| entry.repo_path).collect();
 944                        this.perform_checkout(repo_paths, cx);
 945                    })
 946                    .ok();
 947                }
 948                _ => {
 949                    return;
 950                }
 951            }
 952        })
 953        .detach();
 954    }
 955
 956    fn clean_all(&mut self, _: &TrashUntrackedFiles, window: &mut Window, cx: &mut Context<Self>) {
 957        let workspace = self.workspace.clone();
 958        let Some(active_repo) = self.active_repository.clone() else {
 959            return;
 960        };
 961        let to_delete = self
 962            .entries
 963            .iter()
 964            .filter_map(|entry| entry.status_entry())
 965            .filter(|status_entry| status_entry.status.is_created())
 966            .cloned()
 967            .collect::<Vec<_>>();
 968
 969        match to_delete.len() {
 970            0 => return,
 971            1 => return self.revert_entry(&to_delete[0], window, cx),
 972            _ => {}
 973        };
 974
 975        let mut details = to_delete
 976            .iter()
 977            .map(|entry| {
 978                entry
 979                    .repo_path
 980                    .0
 981                    .file_name()
 982                    .map(|f| f.to_string_lossy())
 983                    .unwrap_or_default()
 984            })
 985            .take(5)
 986            .join("\n");
 987
 988        if to_delete.len() > 5 {
 989            details.push_str(&format!("\nand {} more…", to_delete.len() - 5))
 990        }
 991
 992        let prompt = prompt("Trash these files?", Some(&details), window, cx);
 993        cx.spawn_in(window, |this, mut cx| async move {
 994            match prompt.await? {
 995                TrashCancel::Trash => {}
 996                TrashCancel::Cancel => return Ok(()),
 997            }
 998            let tasks = workspace.update(&mut cx, |workspace, cx| {
 999                to_delete
1000                    .iter()
1001                    .filter_map(|entry| {
1002                        workspace.project().update(cx, |project, cx| {
1003                            let project_path = active_repo
1004                                .read(cx)
1005                                .repo_path_to_project_path(&entry.repo_path)?;
1006                            project.delete_file(project_path, true, cx)
1007                        })
1008                    })
1009                    .collect::<Vec<_>>()
1010            })?;
1011            let to_unstage = to_delete
1012                .into_iter()
1013                .filter_map(|entry| {
1014                    if entry.status.is_staged() != Some(false) {
1015                        Some(entry.repo_path.clone())
1016                    } else {
1017                        None
1018                    }
1019                })
1020                .collect();
1021            this.update(&mut cx, |this, cx| {
1022                this.perform_stage(false, to_unstage, cx)
1023            })?;
1024            for task in tasks {
1025                task.await?;
1026            }
1027            Ok(())
1028        })
1029        .detach_and_prompt_err("Failed to trash files", window, cx, |e, _, _| {
1030            Some(format!("{e}"))
1031        });
1032    }
1033
1034    fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
1035        let repo_paths = self
1036            .entries
1037            .iter()
1038            .filter_map(|entry| entry.status_entry())
1039            .filter(|status_entry| status_entry.is_staged != Some(true))
1040            .map(|status_entry| status_entry.repo_path.clone())
1041            .collect::<Vec<_>>();
1042        self.perform_stage(true, repo_paths, cx);
1043    }
1044
1045    fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
1046        let repo_paths = self
1047            .entries
1048            .iter()
1049            .filter_map(|entry| entry.status_entry())
1050            .filter(|status_entry| status_entry.is_staged != Some(false))
1051            .map(|status_entry| status_entry.repo_path.clone())
1052            .collect::<Vec<_>>();
1053        self.perform_stage(false, repo_paths, cx);
1054    }
1055
1056    fn toggle_staged_for_entry(
1057        &mut self,
1058        entry: &GitListEntry,
1059        _window: &mut Window,
1060        cx: &mut Context<Self>,
1061    ) {
1062        let Some(active_repository) = self.active_repository.as_ref() else {
1063            return;
1064        };
1065        let (stage, repo_paths) = match entry {
1066            GitListEntry::GitStatusEntry(status_entry) => {
1067                if status_entry.status.is_staged().unwrap_or(false) {
1068                    (false, vec![status_entry.repo_path.clone()])
1069                } else {
1070                    (true, vec![status_entry.repo_path.clone()])
1071                }
1072            }
1073            GitListEntry::Header(section) => {
1074                let goal_staged_state = !self.header_state(section.header).selected();
1075                let repository = active_repository.read(cx);
1076                let entries = self
1077                    .entries
1078                    .iter()
1079                    .filter_map(|entry| entry.status_entry())
1080                    .filter(|status_entry| {
1081                        section.contains(&status_entry, repository)
1082                            && status_entry.is_staged != Some(goal_staged_state)
1083                    })
1084                    .map(|status_entry| status_entry.repo_path.clone())
1085                    .collect::<Vec<_>>();
1086
1087                (goal_staged_state, entries)
1088            }
1089        };
1090        self.perform_stage(stage, repo_paths, cx);
1091    }
1092
1093    fn perform_stage(&mut self, stage: bool, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
1094        let Some(active_repository) = self.active_repository.clone() else {
1095            return;
1096        };
1097        let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
1098        self.pending.push(PendingOperation {
1099            op_id,
1100            target_status: if stage {
1101                TargetStatus::Staged
1102            } else {
1103                TargetStatus::Unstaged
1104            },
1105            repo_paths: repo_paths.iter().cloned().collect(),
1106            finished: false,
1107        });
1108        let repo_paths = repo_paths.clone();
1109        let repository = active_repository.read(cx);
1110        self.update_counts(repository);
1111        cx.notify();
1112
1113        cx.spawn({
1114            |this, mut cx| async move {
1115                let result = cx
1116                    .update(|cx| {
1117                        if stage {
1118                            active_repository
1119                                .update(cx, |repo, cx| repo.stage_entries(repo_paths.clone(), cx))
1120                        } else {
1121                            active_repository
1122                                .update(cx, |repo, cx| repo.unstage_entries(repo_paths.clone(), cx))
1123                        }
1124                    })?
1125                    .await;
1126
1127                this.update(&mut cx, |this, cx| {
1128                    for pending in this.pending.iter_mut() {
1129                        if pending.op_id == op_id {
1130                            pending.finished = true
1131                        }
1132                    }
1133                    result
1134                        .map_err(|e| {
1135                            this.show_err_toast(e, cx);
1136                        })
1137                        .ok();
1138                    cx.notify();
1139                })
1140            }
1141        })
1142        .detach();
1143    }
1144
1145    pub fn total_staged_count(&self) -> usize {
1146        self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
1147    }
1148
1149    pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
1150        self.commit_editor
1151            .read(cx)
1152            .buffer()
1153            .read(cx)
1154            .as_singleton()
1155            .unwrap()
1156            .clone()
1157    }
1158
1159    fn toggle_staged_for_selected(
1160        &mut self,
1161        _: &git::ToggleStaged,
1162        window: &mut Window,
1163        cx: &mut Context<Self>,
1164    ) {
1165        if let Some(selected_entry) = self.get_selected_entry().cloned() {
1166            self.toggle_staged_for_entry(&selected_entry, window, cx);
1167        }
1168    }
1169
1170    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
1171        if self
1172            .commit_editor
1173            .focus_handle(cx)
1174            .contains_focused(window, cx)
1175        {
1176            self.commit_changes(window, cx)
1177        } else {
1178            cx.propagate();
1179        }
1180    }
1181
1182    fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
1183        let message = self.commit_editor.read(cx).text(cx);
1184
1185        if !message.trim().is_empty() {
1186            return Some(message.to_string());
1187        }
1188
1189        self.suggest_commit_message()
1190            .filter(|message| !message.trim().is_empty())
1191    }
1192
1193    pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1194        let Some(active_repository) = self.active_repository.clone() else {
1195            return;
1196        };
1197        let error_spawn = |message, window: &mut Window, cx: &mut App| {
1198            let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
1199            cx.spawn(|_| async move {
1200                prompt.await.ok();
1201            })
1202            .detach();
1203        };
1204
1205        if self.has_unstaged_conflicts() {
1206            error_spawn(
1207                "There are still conflicts. You must stage these before committing",
1208                window,
1209                cx,
1210            );
1211            return;
1212        }
1213
1214        let commit_message = self.custom_or_suggested_commit_message(cx);
1215
1216        let Some(mut message) = commit_message else {
1217            self.commit_editor.read(cx).focus_handle(cx).focus(window);
1218            return;
1219        };
1220
1221        if self.add_coauthors {
1222            self.fill_co_authors(&mut message, cx);
1223        }
1224
1225        let task = if self.has_staged_changes() {
1226            // Repository serializes all git operations, so we can just send a commit immediately
1227            let commit_task = active_repository.read(cx).commit(message.into(), None);
1228            cx.background_spawn(async move { commit_task.await? })
1229        } else {
1230            let changed_files = self
1231                .entries
1232                .iter()
1233                .filter_map(|entry| entry.status_entry())
1234                .filter(|status_entry| !status_entry.status.is_created())
1235                .map(|status_entry| status_entry.repo_path.clone())
1236                .collect::<Vec<_>>();
1237
1238            if changed_files.is_empty() {
1239                error_spawn("No changes to commit", window, cx);
1240                return;
1241            }
1242
1243            let stage_task =
1244                active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx));
1245            cx.spawn(|_, mut cx| async move {
1246                stage_task.await?;
1247                let commit_task = active_repository
1248                    .update(&mut cx, |repo, _| repo.commit(message.into(), None))?;
1249                commit_task.await?
1250            })
1251        };
1252        let task = cx.spawn_in(window, |this, mut cx| async move {
1253            let result = task.await;
1254            this.update_in(&mut cx, |this, window, cx| {
1255                this.pending_commit.take();
1256                match result {
1257                    Ok(()) => {
1258                        this.commit_editor
1259                            .update(cx, |editor, cx| editor.clear(window, cx));
1260                    }
1261                    Err(e) => this.show_err_toast(e, cx),
1262                }
1263            })
1264            .ok();
1265        });
1266
1267        self.pending_commit = Some(task);
1268    }
1269
1270    fn uncommit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1271        let Some(repo) = self.active_repository.clone() else {
1272            return;
1273        };
1274
1275        let confirmation = self.check_for_pushed_commits(window, cx);
1276        let prior_head = self.load_commit_details("HEAD", cx);
1277
1278        let task = cx.spawn_in(window, |this, mut cx| async move {
1279            let result = maybe!(async {
1280                if let Ok(true) = confirmation.await {
1281                    let prior_head = prior_head.await?;
1282
1283                    repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
1284                        .await??;
1285
1286                    Ok(Some(prior_head))
1287                } else {
1288                    Ok(None)
1289                }
1290            })
1291            .await;
1292
1293            this.update_in(&mut cx, |this, window, cx| {
1294                this.pending_commit.take();
1295                match result {
1296                    Ok(None) => {}
1297                    Ok(Some(prior_commit)) => {
1298                        this.commit_editor.update(cx, |editor, cx| {
1299                            editor.set_text(prior_commit.message, window, cx)
1300                        });
1301                    }
1302                    Err(e) => this.show_err_toast(e, cx),
1303                }
1304            })
1305            .ok();
1306        });
1307
1308        self.pending_commit = Some(task);
1309    }
1310
1311    fn check_for_pushed_commits(
1312        &mut self,
1313        window: &mut Window,
1314        cx: &mut Context<Self>,
1315    ) -> impl Future<Output = Result<bool, anyhow::Error>> {
1316        let repo = self.active_repository.clone();
1317        let mut cx = window.to_async(cx);
1318
1319        async move {
1320            let Some(repo) = repo else {
1321                return Err(anyhow::anyhow!("No active repository"));
1322            };
1323
1324            let pushed_to: Vec<SharedString> = repo
1325                .update(&mut cx, |repo, _| repo.check_for_pushed_commits())?
1326                .await??;
1327
1328            if pushed_to.is_empty() {
1329                Ok(true)
1330            } else {
1331                #[derive(strum::EnumIter, strum::VariantNames)]
1332                #[strum(serialize_all = "title_case")]
1333                enum CancelUncommit {
1334                    Uncommit,
1335                    Cancel,
1336                }
1337                let detail = format!(
1338                    "This commit was already pushed to {}.",
1339                    pushed_to.into_iter().join(", ")
1340                );
1341                let result = cx
1342                    .update(|window, cx| prompt("Are you sure?", Some(&detail), window, cx))?
1343                    .await?;
1344
1345                match result {
1346                    CancelUncommit::Cancel => Ok(false),
1347                    CancelUncommit::Uncommit => Ok(true),
1348                }
1349            }
1350        }
1351    }
1352
1353    /// Suggests a commit message based on the changed files and their statuses
1354    pub fn suggest_commit_message(&self) -> Option<String> {
1355        if self.total_staged_count() != 1 {
1356            return None;
1357        }
1358
1359        let entry = self
1360            .entries
1361            .iter()
1362            .find(|entry| match entry.status_entry() {
1363                Some(entry) => entry.is_staged.unwrap_or(false),
1364                _ => false,
1365            })?;
1366
1367        let GitListEntry::GitStatusEntry(git_status_entry) = entry.clone() else {
1368            return None;
1369        };
1370
1371        let action_text = if git_status_entry.status.is_deleted() {
1372            Some("Delete")
1373        } else if git_status_entry.status.is_created() {
1374            Some("Create")
1375        } else if git_status_entry.status.is_modified() {
1376            Some("Update")
1377        } else {
1378            None
1379        }?;
1380
1381        let file_name = git_status_entry
1382            .repo_path
1383            .file_name()
1384            .unwrap_or_default()
1385            .to_string_lossy();
1386
1387        Some(format!("{} {}", action_text, file_name))
1388    }
1389
1390    /// Generates a commit message using an LLM.
1391    fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
1392        let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
1393            return;
1394        };
1395        let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
1396            return;
1397        };
1398
1399        if !provider.is_authenticated(cx) {
1400            return;
1401        }
1402
1403        const PROMPT: &str = include_str!("commit_message_prompt.txt");
1404
1405        // TODO: We need to generate a diff from the actual Git state.
1406        //
1407        // It need not look exactly like the structure below, this is just an example generated by Claude.
1408        let diff_text = "diff --git a/src/main.rs b/src/main.rs
1409index 1234567..abcdef0 100644
1410--- a/src/main.rs
1411+++ b/src/main.rs
1412@@ -10,7 +10,7 @@ fn main() {
1413     println!(\"Hello, world!\");
1414-    let unused_var = 42;
1415+    let important_value = 42;
1416
1417     // Do something with the value
1418-    // TODO: Implement this later
1419+    println!(\"The answer is {}\", important_value);
1420 }";
1421
1422        let request = LanguageModelRequest {
1423            messages: vec![LanguageModelRequestMessage {
1424                role: Role::User,
1425                content: vec![format!("{PROMPT}\n{diff_text}").into()],
1426                cache: false,
1427            }],
1428            tools: Vec::new(),
1429            stop: Vec::new(),
1430            temperature: None,
1431        };
1432
1433        self.generate_commit_message_task = Some(cx.spawn(|this, mut cx| {
1434            async move {
1435                let stream = model.stream_completion_text(request, &cx);
1436                let mut messages = stream.await?;
1437
1438                while let Some(message) = messages.stream.next().await {
1439                    let text = message?;
1440
1441                    this.update(&mut cx, |this, cx| {
1442                        this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1443                            let insert_position = buffer.anchor_before(buffer.len());
1444                            buffer.edit([(insert_position..insert_position, text)], None, cx);
1445                        });
1446                    })?;
1447                }
1448
1449                anyhow::Ok(())
1450            }
1451            .log_err()
1452        }));
1453    }
1454
1455    fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
1456        let suggested_commit_message = self.suggest_commit_message();
1457        let placeholder_text = suggested_commit_message
1458            .as_deref()
1459            .unwrap_or("Enter commit message");
1460
1461        self.commit_editor.update(cx, |editor, cx| {
1462            editor.set_placeholder_text(Arc::from(placeholder_text), cx)
1463        });
1464
1465        cx.notify();
1466    }
1467
1468    pub(crate) fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
1469        let Some(repo) = self.active_repository.clone() else {
1470            return;
1471        };
1472        let guard = self.start_remote_operation();
1473        let fetch = repo.read(cx).fetch();
1474        cx.spawn(|this, mut cx| async move {
1475            let remote_message = fetch.await?;
1476            drop(guard);
1477            this.update(&mut cx, |this, cx| {
1478                match remote_message {
1479                    Ok(remote_message) => {
1480                        this.show_remote_output(RemoteAction::Fetch, remote_message, cx);
1481                    }
1482                    Err(e) => {
1483                        this.show_err_toast(e, cx);
1484                    }
1485                }
1486
1487                anyhow::Ok(())
1488            })
1489            .ok();
1490            anyhow::Ok(())
1491        })
1492        .detach_and_log_err(cx);
1493    }
1494
1495    pub(crate) fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
1496        let Some(repo) = self.active_repository.clone() else {
1497            return;
1498        };
1499        let Some(branch) = repo.read(cx).current_branch() else {
1500            return;
1501        };
1502        let branch = branch.clone();
1503        let remote = self.get_current_remote(window, cx);
1504        cx.spawn(move |this, mut cx| async move {
1505            let remote = match remote.await {
1506                Ok(Some(remote)) => remote,
1507                Ok(None) => {
1508                    return Ok(());
1509                }
1510                Err(e) => {
1511                    log::error!("Failed to get current remote: {}", e);
1512                    this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
1513                        .ok();
1514                    return Ok(());
1515                }
1516            };
1517
1518            let guard = this
1519                .update(&mut cx, |this, _| this.start_remote_operation())
1520                .ok();
1521
1522            let pull = repo.update(&mut cx, |repo, _cx| {
1523                repo.pull(branch.name.clone(), remote.name.clone())
1524            })?;
1525
1526            let remote_message = pull.await?;
1527            drop(guard);
1528
1529            this.update(&mut cx, |this, cx| match remote_message {
1530                Ok(remote_message) => {
1531                    this.show_remote_output(RemoteAction::Pull, remote_message, cx)
1532                }
1533                Err(err) => this.show_err_toast(err, cx),
1534            })
1535            .ok();
1536
1537            anyhow::Ok(())
1538        })
1539        .detach_and_log_err(cx);
1540    }
1541
1542    pub(crate) fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
1543        let Some(repo) = self.active_repository.clone() else {
1544            return;
1545        };
1546        let Some(branch) = repo.read(cx).current_branch() else {
1547            return;
1548        };
1549        let branch = branch.clone();
1550        let options = action.options;
1551        let remote = self.get_current_remote(window, cx);
1552
1553        cx.spawn(move |this, mut cx| async move {
1554            let remote = match remote.await {
1555                Ok(Some(remote)) => remote,
1556                Ok(None) => {
1557                    return Ok(());
1558                }
1559                Err(e) => {
1560                    log::error!("Failed to get current remote: {}", e);
1561                    this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
1562                        .ok();
1563                    return Ok(());
1564                }
1565            };
1566
1567            let guard = this
1568                .update(&mut cx, |this, _| this.start_remote_operation())
1569                .ok();
1570
1571            let push = repo.update(&mut cx, |repo, _cx| {
1572                repo.push(branch.name.clone(), remote.name.clone(), options)
1573            })?;
1574
1575            let remote_output = push.await?;
1576
1577            drop(guard);
1578
1579            this.update(&mut cx, |this, cx| match remote_output {
1580                Ok(remote_message) => {
1581                    this.show_remote_output(RemoteAction::Push(remote), remote_message, cx);
1582                }
1583                Err(e) => {
1584                    this.show_err_toast(e, cx);
1585                }
1586            })?;
1587
1588            anyhow::Ok(())
1589        })
1590        .detach_and_log_err(cx);
1591    }
1592
1593    fn get_current_remote(
1594        &mut self,
1595        window: &mut Window,
1596        cx: &mut Context<Self>,
1597    ) -> impl Future<Output = Result<Option<Remote>>> {
1598        let repo = self.active_repository.clone();
1599        let workspace = self.workspace.clone();
1600        let mut cx = window.to_async(cx);
1601
1602        async move {
1603            let Some(repo) = repo else {
1604                return Err(anyhow::anyhow!("No active repository"));
1605            };
1606
1607            let mut current_remotes: Vec<Remote> = repo
1608                .update(&mut cx, |repo, _| {
1609                    let Some(current_branch) = repo.current_branch() else {
1610                        return Err(anyhow::anyhow!("No active branch"));
1611                    };
1612
1613                    Ok(repo.get_remotes(Some(current_branch.name.to_string())))
1614                })??
1615                .await??;
1616
1617            if current_remotes.len() == 0 {
1618                return Err(anyhow::anyhow!("No active remote"));
1619            } else if current_remotes.len() == 1 {
1620                return Ok(Some(current_remotes.pop().unwrap()));
1621            } else {
1622                let current_remotes: Vec<_> = current_remotes
1623                    .into_iter()
1624                    .map(|remotes| remotes.name)
1625                    .collect();
1626                let selection = cx
1627                    .update(|window, cx| {
1628                        picker_prompt::prompt(
1629                            "Pick which remote to push to",
1630                            current_remotes.clone(),
1631                            workspace,
1632                            window,
1633                            cx,
1634                        )
1635                    })?
1636                    .await?;
1637
1638                Ok(selection.map(|selection| Remote {
1639                    name: current_remotes[selection].clone(),
1640                }))
1641            }
1642        }
1643    }
1644
1645    fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
1646        let mut new_co_authors = Vec::new();
1647        let project = self.project.read(cx);
1648
1649        let Some(room) = self
1650            .workspace
1651            .upgrade()
1652            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned())
1653        else {
1654            return Vec::default();
1655        };
1656
1657        let room = room.read(cx);
1658
1659        for (peer_id, collaborator) in project.collaborators() {
1660            if collaborator.is_host {
1661                continue;
1662            }
1663
1664            let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
1665                continue;
1666            };
1667            if participant.can_write() && participant.user.email.is_some() {
1668                let email = participant.user.email.clone().unwrap();
1669
1670                new_co_authors.push((
1671                    participant
1672                        .user
1673                        .name
1674                        .clone()
1675                        .unwrap_or_else(|| participant.user.github_login.clone()),
1676                    email,
1677                ))
1678            }
1679        }
1680        if !project.is_local() && !project.is_read_only(cx) {
1681            if let Some(user) = room.local_participant_user(cx) {
1682                if let Some(email) = user.email.clone() {
1683                    new_co_authors.push((
1684                        user.name
1685                            .clone()
1686                            .unwrap_or_else(|| user.github_login.clone()),
1687                        email.clone(),
1688                    ))
1689                }
1690            }
1691        }
1692        new_co_authors
1693    }
1694
1695    fn toggle_fill_co_authors(
1696        &mut self,
1697        _: &ToggleFillCoAuthors,
1698        _: &mut Window,
1699        cx: &mut Context<Self>,
1700    ) {
1701        self.add_coauthors = !self.add_coauthors;
1702        cx.notify();
1703    }
1704
1705    fn fill_co_authors(&mut self, message: &mut String, cx: &mut Context<Self>) {
1706        const CO_AUTHOR_PREFIX: &str = "Co-authored-by: ";
1707
1708        let existing_text = message.to_ascii_lowercase();
1709        let lowercase_co_author_prefix = CO_AUTHOR_PREFIX.to_lowercase();
1710        let mut ends_with_co_authors = false;
1711        let existing_co_authors = existing_text
1712            .lines()
1713            .filter_map(|line| {
1714                let line = line.trim();
1715                if line.starts_with(&lowercase_co_author_prefix) {
1716                    ends_with_co_authors = true;
1717                    Some(line)
1718                } else {
1719                    ends_with_co_authors = false;
1720                    None
1721                }
1722            })
1723            .collect::<HashSet<_>>();
1724
1725        let new_co_authors = self
1726            .potential_co_authors(cx)
1727            .into_iter()
1728            .filter(|(_, email)| {
1729                !existing_co_authors
1730                    .iter()
1731                    .any(|existing| existing.contains(email.as_str()))
1732            })
1733            .collect::<Vec<_>>();
1734
1735        if new_co_authors.is_empty() {
1736            return;
1737        }
1738
1739        if !ends_with_co_authors {
1740            message.push('\n');
1741        }
1742        for (name, email) in new_co_authors {
1743            message.push('\n');
1744            message.push_str(CO_AUTHOR_PREFIX);
1745            message.push_str(&name);
1746            message.push_str(" <");
1747            message.push_str(&email);
1748            message.push('>');
1749        }
1750        message.push('\n');
1751    }
1752
1753    fn schedule_update(
1754        &mut self,
1755        clear_pending: bool,
1756        window: &mut Window,
1757        cx: &mut Context<Self>,
1758    ) {
1759        let handle = cx.entity().downgrade();
1760        self.reopen_commit_buffer(window, cx);
1761        self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move {
1762            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1763            if let Some(git_panel) = handle.upgrade() {
1764                git_panel
1765                    .update_in(&mut cx, |git_panel, _, cx| {
1766                        if clear_pending {
1767                            git_panel.clear_pending();
1768                        }
1769                        git_panel.update_visible_entries(cx);
1770                        git_panel.update_editor_placeholder(cx);
1771                    })
1772                    .ok();
1773            }
1774        });
1775    }
1776
1777    fn reopen_commit_buffer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1778        let Some(active_repo) = self.active_repository.as_ref() else {
1779            return;
1780        };
1781        let load_buffer = active_repo.update(cx, |active_repo, cx| {
1782            let project = self.project.read(cx);
1783            active_repo.open_commit_buffer(
1784                Some(project.languages().clone()),
1785                project.buffer_store().clone(),
1786                cx,
1787            )
1788        });
1789
1790        cx.spawn_in(window, |git_panel, mut cx| async move {
1791            let buffer = load_buffer.await?;
1792            git_panel.update_in(&mut cx, |git_panel, window, cx| {
1793                if git_panel
1794                    .commit_editor
1795                    .read(cx)
1796                    .buffer()
1797                    .read(cx)
1798                    .as_singleton()
1799                    .as_ref()
1800                    != Some(&buffer)
1801                {
1802                    git_panel.commit_editor = cx.new(|cx| {
1803                        commit_message_editor(
1804                            buffer,
1805                            git_panel.suggest_commit_message().as_deref(),
1806                            git_panel.project.clone(),
1807                            true,
1808                            window,
1809                            cx,
1810                        )
1811                    });
1812                }
1813            })
1814        })
1815        .detach_and_log_err(cx);
1816    }
1817
1818    fn clear_pending(&mut self) {
1819        self.pending.retain(|v| !v.finished)
1820    }
1821
1822    fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
1823        self.entries.clear();
1824        let mut changed_entries = Vec::new();
1825        let mut new_entries = Vec::new();
1826        let mut conflict_entries = Vec::new();
1827
1828        let Some(repo) = self.active_repository.as_ref() else {
1829            // Just clear entries if no repository is active.
1830            cx.notify();
1831            return;
1832        };
1833
1834        // First pass - collect all paths
1835        let repo = repo.read(cx);
1836
1837        // Second pass - create entries with proper depth calculation
1838        for entry in repo.status() {
1839            let is_conflict = repo.has_conflict(&entry.repo_path);
1840            let is_new = entry.status.is_created();
1841            let is_staged = entry.status.is_staged();
1842
1843            if self.pending.iter().any(|pending| {
1844                pending.target_status == TargetStatus::Reverted
1845                    && !pending.finished
1846                    && pending.repo_paths.contains(&entry.repo_path)
1847            }) {
1848                continue;
1849            }
1850
1851            let entry = GitStatusEntry {
1852                repo_path: entry.repo_path.clone(),
1853                status: entry.status,
1854                is_staged,
1855            };
1856
1857            if is_conflict {
1858                conflict_entries.push(entry);
1859            } else if is_new {
1860                new_entries.push(entry);
1861            } else {
1862                changed_entries.push(entry);
1863            }
1864        }
1865
1866        if conflict_entries.len() > 0 {
1867            self.entries.push(GitListEntry::Header(GitHeaderEntry {
1868                header: Section::Conflict,
1869            }));
1870            self.entries.extend(
1871                conflict_entries
1872                    .into_iter()
1873                    .map(GitListEntry::GitStatusEntry),
1874            );
1875        }
1876
1877        if changed_entries.len() > 0 {
1878            self.entries.push(GitListEntry::Header(GitHeaderEntry {
1879                header: Section::Tracked,
1880            }));
1881            self.entries.extend(
1882                changed_entries
1883                    .into_iter()
1884                    .map(GitListEntry::GitStatusEntry),
1885            );
1886        }
1887        if new_entries.len() > 0 {
1888            self.entries.push(GitListEntry::Header(GitHeaderEntry {
1889                header: Section::New,
1890            }));
1891            self.entries
1892                .extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
1893        }
1894
1895        self.update_counts(repo);
1896
1897        self.select_first_entry_if_none(cx);
1898
1899        cx.notify();
1900    }
1901
1902    fn header_state(&self, header_type: Section) -> ToggleState {
1903        let (staged_count, count) = match header_type {
1904            Section::New => (self.new_staged_count, self.new_count),
1905            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
1906            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
1907        };
1908        if staged_count == 0 {
1909            ToggleState::Unselected
1910        } else if count == staged_count {
1911            ToggleState::Selected
1912        } else {
1913            ToggleState::Indeterminate
1914        }
1915    }
1916
1917    fn update_counts(&mut self, repo: &Repository) {
1918        self.conflicted_count = 0;
1919        self.conflicted_staged_count = 0;
1920        self.new_count = 0;
1921        self.tracked_count = 0;
1922        self.new_staged_count = 0;
1923        self.tracked_staged_count = 0;
1924        for entry in &self.entries {
1925            let Some(status_entry) = entry.status_entry() else {
1926                continue;
1927            };
1928            if repo.has_conflict(&status_entry.repo_path) {
1929                self.conflicted_count += 1;
1930                if self.entry_is_staged(status_entry) != Some(false) {
1931                    self.conflicted_staged_count += 1;
1932                }
1933            } else if status_entry.status.is_created() {
1934                self.new_count += 1;
1935                if self.entry_is_staged(status_entry) != Some(false) {
1936                    self.new_staged_count += 1;
1937                }
1938            } else {
1939                self.tracked_count += 1;
1940                if self.entry_is_staged(status_entry) != Some(false) {
1941                    self.tracked_staged_count += 1;
1942                }
1943            }
1944        }
1945    }
1946
1947    fn entry_is_staged(&self, entry: &GitStatusEntry) -> Option<bool> {
1948        for pending in self.pending.iter().rev() {
1949            if pending.repo_paths.contains(&entry.repo_path) {
1950                match pending.target_status {
1951                    TargetStatus::Staged => return Some(true),
1952                    TargetStatus::Unstaged => return Some(false),
1953                    TargetStatus::Reverted => continue,
1954                    TargetStatus::Unchanged => continue,
1955                }
1956            }
1957        }
1958        entry.is_staged
1959    }
1960
1961    pub(crate) fn has_staged_changes(&self) -> bool {
1962        self.tracked_staged_count > 0
1963            || self.new_staged_count > 0
1964            || self.conflicted_staged_count > 0
1965    }
1966
1967    pub(crate) fn has_unstaged_changes(&self) -> bool {
1968        self.tracked_count > self.tracked_staged_count
1969            || self.new_count > self.new_staged_count
1970            || self.conflicted_count > self.conflicted_staged_count
1971    }
1972
1973    fn has_conflicts(&self) -> bool {
1974        self.conflicted_count > 0
1975    }
1976
1977    fn has_tracked_changes(&self) -> bool {
1978        self.tracked_count > 0
1979    }
1980
1981    pub fn has_unstaged_conflicts(&self) -> bool {
1982        self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
1983    }
1984
1985    fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
1986        let Some(workspace) = self.workspace.upgrade() else {
1987            return;
1988        };
1989        let notif_id = NotificationId::Named("git-operation-error".into());
1990
1991        let mut message = e.to_string().trim().to_string();
1992        let toast;
1993        if message.matches("Authentication failed").count() >= 1 {
1994            message = format!(
1995                "{}\n\n{}",
1996                message, "Please set your credentials via the CLI"
1997            );
1998            toast = Toast::new(notif_id, message);
1999        } else {
2000            toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
2001                window.dispatch_action(workspace::OpenLog.boxed_clone(), cx);
2002            });
2003        }
2004        workspace.update(cx, |workspace, cx| {
2005            workspace.show_toast(toast, cx);
2006        });
2007    }
2008
2009    fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
2010        let Some(workspace) = self.workspace.upgrade() else {
2011            return;
2012        };
2013
2014        let notification_id = NotificationId::Named("git-remote-info".into());
2015
2016        workspace.update(cx, |workspace, cx| {
2017            workspace.show_notification(notification_id.clone(), cx, |cx| {
2018                let workspace = cx.weak_entity();
2019                cx.new(|cx| RemoteOutputToast::new(action, info, notification_id, workspace, cx))
2020            });
2021        });
2022    }
2023
2024    pub fn render_spinner(&self) -> Option<impl IntoElement> {
2025        (!self.pending_remote_operations.borrow().is_empty()).then(|| {
2026            Icon::new(IconName::ArrowCircle)
2027                .size(IconSize::XSmall)
2028                .color(Color::Info)
2029                .with_animation(
2030                    "arrow-circle",
2031                    Animation::new(Duration::from_secs(2)).repeat(),
2032                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2033                )
2034                .into_any_element()
2035        })
2036    }
2037
2038    pub fn can_commit(&self) -> bool {
2039        (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts()
2040    }
2041
2042    pub fn can_stage_all(&self) -> bool {
2043        self.has_unstaged_changes()
2044    }
2045
2046    pub fn can_unstage_all(&self) -> bool {
2047        self.has_staged_changes()
2048    }
2049
2050    pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
2051        let potential_co_authors = self.potential_co_authors(cx);
2052        if potential_co_authors.is_empty() {
2053            None
2054        } else {
2055            Some(
2056                IconButton::new("co-authors", IconName::Person)
2057                    .icon_color(Color::Disabled)
2058                    .selected_icon_color(Color::Selected)
2059                    .toggle_state(self.add_coauthors)
2060                    .tooltip(move |_, cx| {
2061                        let title = format!(
2062                            "Add co-authored-by:{}{}",
2063                            if potential_co_authors.len() == 1 {
2064                                ""
2065                            } else {
2066                                "\n"
2067                            },
2068                            potential_co_authors
2069                                .iter()
2070                                .map(|(name, email)| format!(" {} <{}>", name, email))
2071                                .join("\n")
2072                        );
2073                        Tooltip::simple(title, cx)
2074                    })
2075                    .on_click(cx.listener(|this, _, _, cx| {
2076                        this.add_coauthors = !this.add_coauthors;
2077                        cx.notify();
2078                    }))
2079                    .into_any_element(),
2080            )
2081        }
2082    }
2083
2084    pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
2085        if self.has_unstaged_conflicts() {
2086            (false, "You must resolve conflicts before committing")
2087        } else if !self.has_staged_changes() && !self.has_tracked_changes() {
2088            (
2089                false,
2090                "You must have either staged changes or tracked files to commit",
2091            )
2092        } else if self.pending_commit.is_some() {
2093            (false, "Commit in progress")
2094        } else if self.custom_or_suggested_commit_message(cx).is_none() {
2095            (false, "No commit message")
2096        } else if !self.has_write_access(cx) {
2097            (false, "You do not have write access to this project")
2098        } else {
2099            (true, self.commit_button_title())
2100        }
2101    }
2102
2103    pub fn commit_button_title(&self) -> &'static str {
2104        if self.has_staged_changes() {
2105            "Commit"
2106        } else {
2107            "Commit Tracked"
2108        }
2109    }
2110
2111    pub fn render_footer(
2112        &self,
2113        window: &mut Window,
2114        cx: &mut Context<Self>,
2115    ) -> Option<impl IntoElement> {
2116        let active_repository = self.active_repository.clone()?;
2117        let (can_commit, tooltip) = self.configure_commit_button(cx);
2118        let project = self.project.clone().read(cx);
2119        let panel_editor_style = panel_editor_style(true, window, cx);
2120
2121        let enable_coauthors = self.render_co_authors(cx);
2122        // Note: This is hard-coded to `false` as it is not fully implemented.
2123        let show_generate_commit_message_button = false;
2124
2125        let title = self.commit_button_title();
2126        let editor_focus_handle = self.commit_editor.focus_handle(cx);
2127
2128        let branch = active_repository.read(cx).current_branch().cloned();
2129
2130        let footer_size = px(32.);
2131        let gap = px(8.0);
2132
2133        let max_height = window.line_height() * 5. + gap + footer_size;
2134
2135        let expand_button_size = px(16.);
2136
2137        let git_panel = cx.entity().clone();
2138        let display_name = SharedString::from(Arc::from(
2139            active_repository
2140                .read(cx)
2141                .display_name(project, cx)
2142                .trim_end_matches("/"),
2143        ));
2144
2145        let footer = v_flex()
2146            .child(PanelRepoFooter::new(
2147                "footer-button",
2148                display_name,
2149                branch,
2150                Some(git_panel),
2151            ))
2152            .child(
2153                panel_editor_container(window, cx)
2154                    .id("commit-editor-container")
2155                    .relative()
2156                    .h(max_height)
2157                    // .w_full()
2158                    // .border_t_1()
2159                    // .border_color(cx.theme().colors().border)
2160                    .bg(cx.theme().colors().editor_background)
2161                    .cursor_text()
2162                    .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
2163                        window.focus(&this.commit_editor.focus_handle(cx));
2164                    }))
2165                    .child(
2166                        h_flex()
2167                            .id("commit-footer")
2168                            .absolute()
2169                            .bottom_0()
2170                            .right_2()
2171                            .gap_0p5()
2172                            .h(footer_size)
2173                            .flex_none()
2174                            .when(show_generate_commit_message_button, |parent| {
2175                                parent.child(
2176                                    panel_filled_button("Generate Commit Message").on_click(
2177                                        cx.listener(move |this, _event, _window, cx| {
2178                                            this.generate_commit_message(cx);
2179                                        }),
2180                                    ),
2181                                )
2182                            })
2183                            .children(enable_coauthors)
2184                            .child(
2185                                panel_filled_button(title)
2186                                    .tooltip(move |window, cx| {
2187                                        if can_commit {
2188                                            Tooltip::for_action_in(
2189                                                tooltip,
2190                                                &Commit,
2191                                                &editor_focus_handle,
2192                                                window,
2193                                                cx,
2194                                            )
2195                                        } else {
2196                                            Tooltip::simple(tooltip, cx)
2197                                        }
2198                                    })
2199                                    .disabled(!can_commit || self.modal_open)
2200                                    .on_click({
2201                                        cx.listener(move |this, _: &ClickEvent, window, cx| {
2202                                            this.commit_changes(window, cx)
2203                                        })
2204                                    }),
2205                            ),
2206                    )
2207                    // .when(!self.modal_open, |el| {
2208                    .child(EditorElement::new(&self.commit_editor, panel_editor_style))
2209                    .child(
2210                        div()
2211                            .absolute()
2212                            .top_1()
2213                            .right_2()
2214                            .opacity(0.5)
2215                            .hover(|this| this.opacity(1.0))
2216                            .w(expand_button_size)
2217                            .child(
2218                                panel_icon_button("expand-commit-editor", IconName::Maximize)
2219                                    .icon_size(IconSize::Small)
2220                                    .style(ButtonStyle::Transparent)
2221                                    .width(expand_button_size.into())
2222                                    .on_click(cx.listener({
2223                                        move |_, _, window, cx| {
2224                                            window.dispatch_action(
2225                                                git::ShowCommitEditor.boxed_clone(),
2226                                                cx,
2227                                            )
2228                                        }
2229                                    })),
2230                            ),
2231                    ),
2232            );
2233
2234        Some(footer)
2235    }
2236
2237    fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
2238        let active_repository = self.active_repository.as_ref()?;
2239        let branch = active_repository.read(cx).current_branch()?;
2240        let commit = branch.most_recent_commit.as_ref()?.clone();
2241
2242        let this = cx.entity();
2243        Some(
2244            h_flex()
2245                .items_center()
2246                .py_2()
2247                .px(px(8.))
2248                // .bg(cx.theme().colors().background)
2249                // .border_t_1()
2250                .border_color(cx.theme().colors().border)
2251                .gap_1p5()
2252                .child(
2253                    div()
2254                        .flex_grow()
2255                        .overflow_hidden()
2256                        .max_w(relative(0.6))
2257                        .h_full()
2258                        .child(
2259                            Label::new(commit.subject.clone())
2260                                .size(LabelSize::Small)
2261                                .truncate(),
2262                        )
2263                        .id("commit-msg-hover")
2264                        .hoverable_tooltip(move |window, cx| {
2265                            GitPanelMessageTooltip::new(
2266                                this.clone(),
2267                                commit.sha.clone(),
2268                                window,
2269                                cx,
2270                            )
2271                            .into()
2272                        }),
2273                )
2274                .child(div().flex_1())
2275                .when(commit.has_parent, |this| {
2276                    let has_unstaged = self.has_unstaged_changes();
2277                    this.child(
2278                        panel_icon_button("undo", IconName::Undo)
2279                            .icon_size(IconSize::Small)
2280                            .icon_color(Color::Muted)
2281                            .tooltip(move |window, cx| {
2282                                Tooltip::with_meta(
2283                                    "Uncommit",
2284                                    Some(&git::Uncommit),
2285                                    if has_unstaged {
2286                                        "git reset HEAD^ --soft"
2287                                    } else {
2288                                        "git reset HEAD^"
2289                                    },
2290                                    window,
2291                                    cx,
2292                                )
2293                            })
2294                            .on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
2295                    )
2296                }),
2297        )
2298    }
2299
2300    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
2301        h_flex()
2302            .h_full()
2303            .flex_grow()
2304            .justify_center()
2305            .items_center()
2306            .child(
2307                v_flex()
2308                    .gap_3()
2309                    .child(if self.active_repository.is_some() {
2310                        "No changes to commit"
2311                    } else {
2312                        "No Git repositories"
2313                    })
2314                    .text_ui_sm(cx)
2315                    .mx_auto()
2316                    .text_color(Color::Placeholder.color(cx)),
2317            )
2318    }
2319
2320    fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
2321        let scroll_bar_style = self.show_scrollbar(cx);
2322        let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
2323
2324        if !self.should_show_scrollbar(cx)
2325            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
2326        {
2327            return None;
2328        }
2329
2330        Some(
2331            div()
2332                .id("git-panel-vertical-scroll")
2333                .occlude()
2334                .flex_none()
2335                .h_full()
2336                .cursor_default()
2337                .when(show_container, |this| this.pl_1().px_1p5())
2338                .when(!show_container, |this| {
2339                    this.absolute().right_1().top_1().bottom_1().w(px(12.))
2340                })
2341                .on_mouse_move(cx.listener(|_, _, _, cx| {
2342                    cx.notify();
2343                    cx.stop_propagation()
2344                }))
2345                .on_hover(|_, _, cx| {
2346                    cx.stop_propagation();
2347                })
2348                .on_any_mouse_down(|_, _, cx| {
2349                    cx.stop_propagation();
2350                })
2351                .on_mouse_up(
2352                    MouseButton::Left,
2353                    cx.listener(|this, _, window, cx| {
2354                        if !this.scrollbar_state.is_dragging()
2355                            && !this.focus_handle.contains_focused(window, cx)
2356                        {
2357                            this.hide_scrollbar(window, cx);
2358                            cx.notify();
2359                        }
2360
2361                        cx.stop_propagation();
2362                    }),
2363                )
2364                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2365                    cx.notify();
2366                }))
2367                .children(Scrollbar::vertical(
2368                    // percentage as f32..end_offset as f32,
2369                    self.scrollbar_state.clone(),
2370                )),
2371        )
2372    }
2373
2374    fn render_buffer_header_controls(
2375        &self,
2376        entity: &Entity<Self>,
2377        file: &Arc<dyn File>,
2378        _: &Window,
2379        cx: &App,
2380    ) -> Option<AnyElement> {
2381        let repo = self.active_repository.as_ref()?.read(cx);
2382        let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
2383        let ix = self.entry_by_path(&repo_path)?;
2384        let entry = self.entries.get(ix)?;
2385
2386        let is_staged = self.entry_is_staged(entry.status_entry()?);
2387
2388        let checkbox = Checkbox::new("stage-file", is_staged.into())
2389            .disabled(!self.has_write_access(cx))
2390            .fill()
2391            .elevation(ElevationIndex::Surface)
2392            .on_click({
2393                let entry = entry.clone();
2394                let git_panel = entity.downgrade();
2395                move |_, window, cx| {
2396                    git_panel
2397                        .update(cx, |this, cx| {
2398                            this.toggle_staged_for_entry(&entry, window, cx);
2399                            cx.stop_propagation();
2400                        })
2401                        .ok();
2402                }
2403            });
2404        Some(
2405            h_flex()
2406                .id("start-slot")
2407                .text_lg()
2408                .child(checkbox)
2409                .on_mouse_down(MouseButton::Left, |_, _, cx| {
2410                    // prevent the list item active state triggering when toggling checkbox
2411                    cx.stop_propagation();
2412                })
2413                .into_any_element(),
2414        )
2415    }
2416
2417    fn render_entries(
2418        &self,
2419        has_write_access: bool,
2420        _: &Window,
2421        cx: &mut Context<Self>,
2422    ) -> impl IntoElement {
2423        let entry_count = self.entries.len();
2424
2425        h_flex()
2426            .size_full()
2427            .flex_grow()
2428            .overflow_hidden()
2429            .child(
2430                uniform_list(cx.entity().clone(), "entries", entry_count, {
2431                    move |this, range, window, cx| {
2432                        let mut items = Vec::with_capacity(range.end - range.start);
2433
2434                        for ix in range {
2435                            match &this.entries.get(ix) {
2436                                Some(GitListEntry::GitStatusEntry(entry)) => {
2437                                    items.push(this.render_entry(
2438                                        ix,
2439                                        entry,
2440                                        has_write_access,
2441                                        window,
2442                                        cx,
2443                                    ));
2444                                }
2445                                Some(GitListEntry::Header(header)) => {
2446                                    items.push(this.render_list_header(
2447                                        ix,
2448                                        header,
2449                                        has_write_access,
2450                                        window,
2451                                        cx,
2452                                    ));
2453                                }
2454                                None => {}
2455                            }
2456                        }
2457
2458                        items
2459                    }
2460                })
2461                .size_full()
2462                .with_sizing_behavior(ListSizingBehavior::Auto)
2463                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
2464                .track_scroll(self.scroll_handle.clone()),
2465            )
2466            .on_mouse_down(
2467                MouseButton::Right,
2468                cx.listener(move |this, event: &MouseDownEvent, window, cx| {
2469                    this.deploy_panel_context_menu(event.position, window, cx)
2470                }),
2471            )
2472            .children(self.render_scrollbar(cx))
2473    }
2474
2475    fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
2476        Label::new(label.into()).color(color).single_line()
2477    }
2478
2479    fn list_item_height(&self) -> Rems {
2480        rems(1.75)
2481    }
2482
2483    fn render_list_header(
2484        &self,
2485        ix: usize,
2486        header: &GitHeaderEntry,
2487        _: bool,
2488        _: &Window,
2489        _: &Context<Self>,
2490    ) -> AnyElement {
2491        let id: ElementId = ElementId::Name(format!("header_{}", ix).into());
2492
2493        h_flex()
2494            .id(id)
2495            .h(self.list_item_height())
2496            .w_full()
2497            .items_end()
2498            .px(rems(0.75)) // ~12px
2499            .pb(rems(0.3125)) // ~ 5px
2500            .child(
2501                Label::new(header.title())
2502                    .color(Color::Muted)
2503                    .size(LabelSize::Small)
2504                    .line_height_style(LineHeightStyle::UiLabel)
2505                    .single_line(),
2506            )
2507            .into_any_element()
2508    }
2509
2510    fn load_commit_details(
2511        &self,
2512        sha: &str,
2513        cx: &mut Context<Self>,
2514    ) -> Task<Result<CommitDetails>> {
2515        let Some(repo) = self.active_repository.clone() else {
2516            return Task::ready(Err(anyhow::anyhow!("no active repo")));
2517        };
2518        repo.update(cx, |repo, cx| {
2519            let show = repo.show(sha);
2520            cx.spawn(|_, _| async move { show.await? })
2521        })
2522    }
2523
2524    fn deploy_entry_context_menu(
2525        &mut self,
2526        position: Point<Pixels>,
2527        ix: usize,
2528        window: &mut Window,
2529        cx: &mut Context<Self>,
2530    ) {
2531        let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
2532            return;
2533        };
2534        let stage_title = if entry.status.is_staged() == Some(true) {
2535            "Unstage File"
2536        } else {
2537            "Stage File"
2538        };
2539        let restore_title = if entry.status.is_created() {
2540            "Trash File"
2541        } else {
2542            "Restore File"
2543        };
2544        let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
2545            context_menu
2546                .action(stage_title, ToggleStaged.boxed_clone())
2547                .action(restore_title, git::RestoreFile.boxed_clone())
2548                .separator()
2549                .action("Open Diff", Confirm.boxed_clone())
2550                .action("Open File", SecondaryConfirm.boxed_clone())
2551        });
2552        self.selected_entry = Some(ix);
2553        self.set_context_menu(context_menu, position, window, cx);
2554    }
2555
2556    fn deploy_panel_context_menu(
2557        &mut self,
2558        position: Point<Pixels>,
2559        window: &mut Window,
2560        cx: &mut Context<Self>,
2561    ) {
2562        let context_menu = git_panel_context_menu(window, cx);
2563        self.set_context_menu(context_menu, position, window, cx);
2564    }
2565
2566    fn set_context_menu(
2567        &mut self,
2568        context_menu: Entity<ContextMenu>,
2569        position: Point<Pixels>,
2570        window: &Window,
2571        cx: &mut Context<Self>,
2572    ) {
2573        let subscription = cx.subscribe_in(
2574            &context_menu,
2575            window,
2576            |this, _, _: &DismissEvent, window, cx| {
2577                if this.context_menu.as_ref().is_some_and(|context_menu| {
2578                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
2579                }) {
2580                    cx.focus_self(window);
2581                }
2582                this.context_menu.take();
2583                cx.notify();
2584            },
2585        );
2586        self.context_menu = Some((context_menu, position, subscription));
2587        cx.notify();
2588    }
2589
2590    fn render_entry(
2591        &self,
2592        ix: usize,
2593        entry: &GitStatusEntry,
2594        has_write_access: bool,
2595        window: &Window,
2596        cx: &Context<Self>,
2597    ) -> AnyElement {
2598        let display_name = entry
2599            .repo_path
2600            .file_name()
2601            .map(|name| name.to_string_lossy().into_owned())
2602            .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned());
2603
2604        let repo_path = entry.repo_path.clone();
2605        let selected = self.selected_entry == Some(ix);
2606        let marked = self.marked_entries.contains(&ix);
2607        let status_style = GitPanelSettings::get_global(cx).status_style;
2608        let status = entry.status;
2609        let has_conflict = status.is_conflicted();
2610        let is_modified = status.is_modified();
2611        let is_deleted = status.is_deleted();
2612
2613        let label_color = if status_style == StatusStyle::LabelColor {
2614            if has_conflict {
2615                Color::Conflict
2616            } else if is_modified {
2617                Color::Modified
2618            } else if is_deleted {
2619                // We don't want a bunch of red labels in the list
2620                Color::Disabled
2621            } else {
2622                Color::Created
2623            }
2624        } else {
2625            Color::Default
2626        };
2627
2628        let path_color = if status.is_deleted() {
2629            Color::Disabled
2630        } else {
2631            Color::Muted
2632        };
2633
2634        let id: ElementId = ElementId::Name(format!("entry_{}_{}", display_name, ix).into());
2635        let checkbox_wrapper_id: ElementId =
2636            ElementId::Name(format!("entry_{}_{}_checkbox_wrapper", display_name, ix).into());
2637        let checkbox_id: ElementId =
2638            ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
2639
2640        let is_entry_staged = self.entry_is_staged(entry);
2641        let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
2642
2643        if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() {
2644            is_staged = ToggleState::Selected;
2645        }
2646
2647        let handle = cx.weak_entity();
2648
2649        let selected_bg_alpha = 0.08;
2650        let marked_bg_alpha = 0.12;
2651        let state_opacity_step = 0.04;
2652
2653        let base_bg = match (selected, marked) {
2654            (true, true) => cx
2655                .theme()
2656                .status()
2657                .info
2658                .alpha(selected_bg_alpha + marked_bg_alpha),
2659            (true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
2660            (false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
2661            _ => cx.theme().colors().ghost_element_background,
2662        };
2663
2664        let hover_bg = if selected {
2665            cx.theme()
2666                .status()
2667                .info
2668                .alpha(selected_bg_alpha + state_opacity_step)
2669        } else {
2670            cx.theme().colors().ghost_element_hover
2671        };
2672
2673        let active_bg = if selected {
2674            cx.theme()
2675                .status()
2676                .info
2677                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
2678        } else {
2679            cx.theme().colors().ghost_element_active
2680        };
2681
2682        h_flex()
2683            .id(id)
2684            .h(self.list_item_height())
2685            .w_full()
2686            .items_center()
2687            .border_1()
2688            .when(selected && self.focus_handle.is_focused(window), |el| {
2689                el.border_color(cx.theme().colors().border_focused)
2690            })
2691            .px(rems(0.75)) // ~12px
2692            .overflow_hidden()
2693            .flex_none()
2694            .gap(DynamicSpacing::Base04.rems(cx))
2695            .bg(base_bg)
2696            .hover(|this| this.bg(hover_bg))
2697            .active(|this| this.bg(active_bg))
2698            .on_click({
2699                cx.listener(move |this, event: &ClickEvent, window, cx| {
2700                    this.selected_entry = Some(ix);
2701                    cx.notify();
2702                    if event.modifiers().secondary() {
2703                        this.open_file(&Default::default(), window, cx)
2704                    } else {
2705                        this.open_diff(&Default::default(), window, cx);
2706                        this.focus_handle.focus(window);
2707                    }
2708                })
2709            })
2710            .on_mouse_down(
2711                MouseButton::Right,
2712                move |event: &MouseDownEvent, window, cx| {
2713                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
2714                    if event.button != MouseButton::Right {
2715                        return;
2716                    }
2717
2718                    let Some(this) = handle.upgrade() else {
2719                        return;
2720                    };
2721                    this.update(cx, |this, cx| {
2722                        this.deploy_entry_context_menu(event.position, ix, window, cx);
2723                    });
2724                    cx.stop_propagation();
2725                },
2726            )
2727            // .on_secondary_mouse_down(cx.listener(
2728            //     move |this, event: &MouseDownEvent, window, cx| {
2729            //         this.deploy_entry_context_menu(event.position, ix, window, cx);
2730            //         cx.stop_propagation();
2731            //     },
2732            // ))
2733            .child(
2734                div()
2735                    .id(checkbox_wrapper_id)
2736                    .flex_none()
2737                    .occlude()
2738                    .cursor_pointer()
2739                    .child(
2740                        Checkbox::new(checkbox_id, is_staged)
2741                            .disabled(!has_write_access)
2742                            .fill()
2743                            .placeholder(!self.has_staged_changes() && !self.has_conflicts())
2744                            .elevation(ElevationIndex::Surface)
2745                            .on_click({
2746                                let entry = entry.clone();
2747                                cx.listener(move |this, _, window, cx| {
2748                                    if !has_write_access {
2749                                        return;
2750                                    }
2751                                    this.toggle_staged_for_entry(
2752                                        &GitListEntry::GitStatusEntry(entry.clone()),
2753                                        window,
2754                                        cx,
2755                                    );
2756                                    cx.stop_propagation();
2757                                })
2758                            })
2759                            .tooltip(move |window, cx| {
2760                                let tooltip_name = if is_entry_staged.unwrap_or(false) {
2761                                    "Unstage"
2762                                } else {
2763                                    "Stage"
2764                                };
2765
2766                                Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
2767                            }),
2768                    ),
2769            )
2770            .child(git_status_icon(status, cx))
2771            .child(
2772                h_flex()
2773                    .items_center()
2774                    .overflow_hidden()
2775                    .when_some(repo_path.parent(), |this, parent| {
2776                        let parent_str = parent.to_string_lossy();
2777                        if !parent_str.is_empty() {
2778                            this.child(
2779                                self.entry_label(format!("{}/", parent_str), path_color)
2780                                    .when(status.is_deleted(), |this| this.strikethrough()),
2781                            )
2782                        } else {
2783                            this
2784                        }
2785                    })
2786                    .child(
2787                        self.entry_label(display_name.clone(), label_color)
2788                            .when(status.is_deleted(), |this| this.strikethrough()),
2789                    ),
2790            )
2791            .into_any_element()
2792    }
2793
2794    fn has_write_access(&self, cx: &App) -> bool {
2795        !self.project.read(cx).is_read_only(cx)
2796    }
2797}
2798
2799impl Render for GitPanel {
2800    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2801        let project = self.project.read(cx);
2802        let has_entries = self.entries.len() > 0;
2803        let room = self
2804            .workspace
2805            .upgrade()
2806            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned());
2807
2808        let has_write_access = self.has_write_access(cx);
2809
2810        let has_co_authors = room.map_or(false, |room| {
2811            room.read(cx)
2812                .remote_participants()
2813                .values()
2814                .any(|remote_participant| remote_participant.can_write())
2815        });
2816
2817        v_flex()
2818            .id("git_panel")
2819            .key_context(self.dispatch_context(window, cx))
2820            .track_focus(&self.focus_handle)
2821            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
2822            .when(has_write_access && !project.is_read_only(cx), |this| {
2823                this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
2824                    this.toggle_staged_for_selected(&ToggleStaged, window, cx)
2825                }))
2826                .on_action(cx.listener(GitPanel::commit))
2827            })
2828            .on_action(cx.listener(Self::select_first))
2829            .on_action(cx.listener(Self::select_next))
2830            .on_action(cx.listener(Self::select_previous))
2831            .on_action(cx.listener(Self::select_last))
2832            .on_action(cx.listener(Self::close_panel))
2833            .on_action(cx.listener(Self::open_diff))
2834            .on_action(cx.listener(Self::open_file))
2835            .on_action(cx.listener(Self::revert_selected))
2836            .on_action(cx.listener(Self::focus_changes_list))
2837            .on_action(cx.listener(Self::focus_editor))
2838            .on_action(cx.listener(Self::toggle_staged_for_selected))
2839            .on_action(cx.listener(Self::stage_all))
2840            .on_action(cx.listener(Self::unstage_all))
2841            .on_action(cx.listener(Self::restore_tracked_files))
2842            .on_action(cx.listener(Self::clean_all))
2843            .when(has_write_access && has_co_authors, |git_panel| {
2844                git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
2845            })
2846            // .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
2847            .on_hover(cx.listener(|this, hovered, window, cx| {
2848                if *hovered {
2849                    this.show_scrollbar = true;
2850                    this.hide_scrollbar_task.take();
2851                    cx.notify();
2852                } else if !this.focus_handle.contains_focused(window, cx) {
2853                    this.hide_scrollbar(window, cx);
2854                }
2855            }))
2856            .size_full()
2857            .overflow_hidden()
2858            .bg(ElevationIndex::Surface.bg(cx))
2859            .child(
2860                v_flex()
2861                    .size_full()
2862                    .map(|this| {
2863                        if has_entries {
2864                            this.child(self.render_entries(has_write_access, window, cx))
2865                        } else {
2866                            this.child(self.render_empty_state(cx).into_any_element())
2867                        }
2868                    })
2869                    .children(self.render_footer(window, cx))
2870                    .children(self.render_previous_commit(cx))
2871                    .into_any_element(),
2872            )
2873            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2874                deferred(
2875                    anchored()
2876                        .position(*position)
2877                        .anchor(gpui::Corner::TopLeft)
2878                        .child(menu.clone()),
2879                )
2880                .with_priority(1)
2881            }))
2882    }
2883}
2884
2885impl Focusable for GitPanel {
2886    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
2887        self.focus_handle.clone()
2888    }
2889}
2890
2891impl EventEmitter<Event> for GitPanel {}
2892
2893impl EventEmitter<PanelEvent> for GitPanel {}
2894
2895pub(crate) struct GitPanelAddon {
2896    pub(crate) workspace: WeakEntity<Workspace>,
2897}
2898
2899impl editor::Addon for GitPanelAddon {
2900    fn to_any(&self) -> &dyn std::any::Any {
2901        self
2902    }
2903
2904    fn render_buffer_header_controls(
2905        &self,
2906        excerpt_info: &ExcerptInfo,
2907        window: &Window,
2908        cx: &App,
2909    ) -> Option<AnyElement> {
2910        let file = excerpt_info.buffer.file()?;
2911        let git_panel = self.workspace.upgrade()?.read(cx).panel::<GitPanel>(cx)?;
2912
2913        git_panel
2914            .read(cx)
2915            .render_buffer_header_controls(&git_panel, &file, window, cx)
2916    }
2917}
2918
2919impl Panel for GitPanel {
2920    fn persistent_name() -> &'static str {
2921        "GitPanel"
2922    }
2923
2924    fn position(&self, _: &Window, cx: &App) -> DockPosition {
2925        GitPanelSettings::get_global(cx).dock
2926    }
2927
2928    fn position_is_valid(&self, position: DockPosition) -> bool {
2929        matches!(position, DockPosition::Left | DockPosition::Right)
2930    }
2931
2932    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
2933        settings::update_settings_file::<GitPanelSettings>(
2934            self.fs.clone(),
2935            cx,
2936            move |settings, _| settings.dock = Some(position),
2937        );
2938    }
2939
2940    fn size(&self, _: &Window, cx: &App) -> Pixels {
2941        self.width
2942            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
2943    }
2944
2945    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
2946        self.width = size;
2947        self.serialize(cx);
2948        cx.notify();
2949    }
2950
2951    fn icon(&self, _: &Window, cx: &App) -> Option<ui::IconName> {
2952        Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
2953    }
2954
2955    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2956        Some("Git Panel")
2957    }
2958
2959    fn toggle_action(&self) -> Box<dyn Action> {
2960        Box::new(ToggleFocus)
2961    }
2962
2963    fn activation_priority(&self) -> u32 {
2964        2
2965    }
2966}
2967
2968impl PanelHeader for GitPanel {}
2969
2970struct GitPanelMessageTooltip {
2971    commit_tooltip: Option<Entity<CommitTooltip>>,
2972}
2973
2974impl GitPanelMessageTooltip {
2975    fn new(
2976        git_panel: Entity<GitPanel>,
2977        sha: SharedString,
2978        window: &mut Window,
2979        cx: &mut App,
2980    ) -> Entity<Self> {
2981        cx.new(|cx| {
2982            cx.spawn_in(window, |this, mut cx| async move {
2983                let details = git_panel
2984                    .update(&mut cx, |git_panel, cx| {
2985                        git_panel.load_commit_details(&sha, cx)
2986                    })?
2987                    .await?;
2988
2989                let commit_details = editor::commit_tooltip::CommitDetails {
2990                    sha: details.sha.clone(),
2991                    committer_name: details.committer_name.clone(),
2992                    committer_email: details.committer_email.clone(),
2993                    commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
2994                    message: Some(editor::commit_tooltip::ParsedCommitMessage {
2995                        message: details.message.clone(),
2996                        ..Default::default()
2997                    }),
2998                };
2999
3000                this.update_in(&mut cx, |this: &mut GitPanelMessageTooltip, window, cx| {
3001                    this.commit_tooltip =
3002                        Some(cx.new(move |cx| CommitTooltip::new(commit_details, window, cx)));
3003                    cx.notify();
3004                })
3005            })
3006            .detach();
3007
3008            Self {
3009                commit_tooltip: None,
3010            }
3011        })
3012    }
3013}
3014
3015impl Render for GitPanelMessageTooltip {
3016    fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement {
3017        if let Some(commit_tooltip) = &self.commit_tooltip {
3018            commit_tooltip.clone().into_any_element()
3019        } else {
3020            gpui::Empty.into_any_element()
3021        }
3022    }
3023}
3024
3025fn git_action_tooltip(
3026    label: impl Into<SharedString>,
3027    action: &dyn Action,
3028    command: impl Into<SharedString>,
3029    focus_handle: Option<FocusHandle>,
3030    window: &mut Window,
3031    cx: &mut App,
3032) -> AnyView {
3033    let label = label.into();
3034    let command = command.into();
3035
3036    if let Some(handle) = focus_handle {
3037        Tooltip::with_meta_in(
3038            label.clone(),
3039            Some(action),
3040            command.clone(),
3041            &handle,
3042            window,
3043            cx,
3044        )
3045    } else {
3046        Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
3047    }
3048}
3049
3050#[derive(IntoElement)]
3051struct SplitButton {
3052    pub left: ButtonLike,
3053    pub right: AnyElement,
3054}
3055
3056impl SplitButton {
3057    fn new(
3058        id: impl Into<SharedString>,
3059        left_label: impl Into<SharedString>,
3060        ahead_count: usize,
3061        behind_count: usize,
3062        left_icon: Option<IconName>,
3063        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
3064        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
3065    ) -> Self {
3066        let id = id.into();
3067
3068        fn count(count: usize) -> impl IntoElement {
3069            h_flex()
3070                .ml_neg_px()
3071                .h(rems(0.875))
3072                .items_center()
3073                .overflow_hidden()
3074                .px_0p5()
3075                .child(
3076                    Label::new(count.to_string())
3077                        .size(LabelSize::XSmall)
3078                        .line_height_style(LineHeightStyle::UiLabel),
3079                )
3080        }
3081
3082        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
3083
3084        let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
3085            format!("split-button-left-{}", id).into(),
3086        ))
3087        .layer(ui::ElevationIndex::ModalSurface)
3088        .size(ui::ButtonSize::Compact)
3089        .when(should_render_counts, |this| {
3090            this.child(
3091                h_flex()
3092                    .ml_neg_0p5()
3093                    .mr_1()
3094                    .when(behind_count > 0, |this| {
3095                        this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
3096                            .child(count(behind_count))
3097                    })
3098                    .when(ahead_count > 0, |this| {
3099                        this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
3100                            .child(count(ahead_count))
3101                    }),
3102            )
3103        })
3104        .when_some(left_icon, |this, left_icon| {
3105            this.child(
3106                h_flex()
3107                    .ml_neg_0p5()
3108                    .mr_1()
3109                    .child(Icon::new(left_icon).size(IconSize::XSmall)),
3110            )
3111        })
3112        .child(
3113            div()
3114                .child(Label::new(left_label).size(LabelSize::Small))
3115                .mr_0p5(),
3116        )
3117        .on_click(left_on_click)
3118        .tooltip(tooltip);
3119
3120        let right =
3121            render_git_action_menu(ElementId::Name(format!("split-button-right-{}", id).into()))
3122                .into_any_element();
3123        // .on_click(right_on_click);
3124
3125        Self { left, right }
3126    }
3127}
3128
3129impl RenderOnce for SplitButton {
3130    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
3131        h_flex()
3132            .rounded_md()
3133            .border_1()
3134            .border_color(cx.theme().colors().text_muted.alpha(0.12))
3135            .child(self.left)
3136            .child(
3137                div()
3138                    .h_full()
3139                    .w_px()
3140                    .bg(cx.theme().colors().text_muted.alpha(0.16)),
3141            )
3142            .child(self.right)
3143            .bg(ElevationIndex::Surface.on_elevation_bg(cx))
3144            .shadow(smallvec![BoxShadow {
3145                color: hsla(0.0, 0.0, 0.0, 0.16),
3146                offset: point(px(0.), px(1.)),
3147                blur_radius: px(0.),
3148                spread_radius: px(0.),
3149            }])
3150    }
3151}
3152
3153fn render_git_action_menu(id: impl Into<ElementId>) -> impl IntoElement {
3154    PopoverMenu::new(id.into())
3155        .trigger(
3156            ui::ButtonLike::new_rounded_right("split-button-right")
3157                .layer(ui::ElevationIndex::ModalSurface)
3158                .size(ui::ButtonSize::None)
3159                .child(
3160                    div()
3161                        .px_1()
3162                        .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
3163                ),
3164        )
3165        .menu(move |window, cx| {
3166            Some(ContextMenu::build(window, cx, |context_menu, _, _| {
3167                context_menu
3168                    .action("Fetch", git::Fetch.boxed_clone())
3169                    .action("Pull", git::Pull.boxed_clone())
3170                    .separator()
3171                    .action("Push", git::Push { options: None }.boxed_clone())
3172                    .action(
3173                        "Force Push",
3174                        git::Push {
3175                            options: Some(PushOptions::Force),
3176                        }
3177                        .boxed_clone(),
3178                    )
3179            }))
3180        })
3181        .anchor(Corner::TopRight)
3182}
3183
3184#[derive(IntoElement, IntoComponent)]
3185#[component(scope = "git_panel")]
3186pub struct PanelRepoFooter {
3187    id: SharedString,
3188    active_repository: SharedString,
3189    branch: Option<Branch>,
3190    // Getting a GitPanel in previews will be difficult.
3191    //
3192    // For now just take an option here, and we won't bind handlers to buttons in previews.
3193    git_panel: Option<Entity<GitPanel>>,
3194}
3195
3196impl PanelRepoFooter {
3197    pub fn new(
3198        id: impl Into<SharedString>,
3199        active_repository: SharedString,
3200        branch: Option<Branch>,
3201        git_panel: Option<Entity<GitPanel>>,
3202    ) -> Self {
3203        Self {
3204            id: id.into(),
3205            active_repository,
3206            branch,
3207            git_panel,
3208        }
3209    }
3210
3211    pub fn new_preview(
3212        id: impl Into<SharedString>,
3213        active_repository: SharedString,
3214        branch: Option<Branch>,
3215    ) -> Self {
3216        Self {
3217            id: id.into(),
3218            active_repository,
3219            branch,
3220            git_panel: None,
3221        }
3222    }
3223
3224    fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
3225        PopoverMenu::new(id.into())
3226            .trigger(
3227                IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
3228                    .icon_size(IconSize::Small)
3229                    .icon_color(Color::Muted),
3230            )
3231            .menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
3232            .anchor(Corner::TopRight)
3233    }
3234
3235    fn panel_focus_handle(&self, cx: &App) -> Option<FocusHandle> {
3236        if let Some(git_panel) = self.git_panel.clone() {
3237            Some(git_panel.focus_handle(cx))
3238        } else {
3239            None
3240        }
3241    }
3242
3243    fn render_push_button(&self, id: SharedString, ahead: u32, cx: &mut App) -> SplitButton {
3244        let panel = self.git_panel.clone();
3245        let panel_focus_handle = self.panel_focus_handle(cx);
3246
3247        SplitButton::new(
3248            id,
3249            "Push",
3250            ahead as usize,
3251            0,
3252            None,
3253            move |_, window, cx| {
3254                if let Some(panel) = panel.as_ref() {
3255                    panel.update(cx, |panel, cx| {
3256                        panel.push(&git::Push { options: None }, window, cx);
3257                    });
3258                }
3259            },
3260            move |window, cx| {
3261                git_action_tooltip(
3262                    "Push committed changes to remote",
3263                    &git::Push { options: None },
3264                    "git push",
3265                    panel_focus_handle.clone(),
3266                    window,
3267                    cx,
3268                )
3269            },
3270        )
3271    }
3272
3273    fn render_pull_button(
3274        &self,
3275        id: SharedString,
3276        ahead: u32,
3277        behind: u32,
3278        cx: &mut App,
3279    ) -> SplitButton {
3280        let panel = self.git_panel.clone();
3281        let panel_focus_handle = self.panel_focus_handle(cx);
3282
3283        SplitButton::new(
3284            id,
3285            "Pull",
3286            ahead as usize,
3287            behind as usize,
3288            None,
3289            move |_, window, cx| {
3290                if let Some(panel) = panel.as_ref() {
3291                    panel.update(cx, |panel, cx| {
3292                        panel.pull(&git::Pull, window, cx);
3293                    });
3294                }
3295            },
3296            move |window, cx| {
3297                git_action_tooltip(
3298                    "Pull",
3299                    &git::Pull,
3300                    "git pull",
3301                    panel_focus_handle.clone(),
3302                    window,
3303                    cx,
3304                )
3305            },
3306        )
3307    }
3308
3309    fn render_fetch_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
3310        let panel = self.git_panel.clone();
3311        let panel_focus_handle = self.panel_focus_handle(cx);
3312
3313        SplitButton::new(
3314            id,
3315            "Fetch",
3316            0,
3317            0,
3318            Some(IconName::ArrowCircle),
3319            move |_, window, cx| {
3320                if let Some(panel) = panel.as_ref() {
3321                    panel.update(cx, |panel, cx| {
3322                        panel.fetch(&git::Fetch, window, cx);
3323                    });
3324                }
3325            },
3326            move |window, cx| {
3327                git_action_tooltip(
3328                    "Fetch updates from remote",
3329                    &git::Fetch,
3330                    "git fetch",
3331                    panel_focus_handle.clone(),
3332                    window,
3333                    cx,
3334                )
3335            },
3336        )
3337    }
3338
3339    fn render_publish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
3340        let panel = self.git_panel.clone();
3341        let panel_focus_handle = self.panel_focus_handle(cx);
3342
3343        SplitButton::new(
3344            id,
3345            "Publish",
3346            0,
3347            0,
3348            Some(IconName::ArrowUpFromLine),
3349            move |_, window, cx| {
3350                if let Some(panel) = panel.as_ref() {
3351                    panel.update(cx, |panel, cx| {
3352                        panel.push(
3353                            &git::Push {
3354                                options: Some(PushOptions::SetUpstream),
3355                            },
3356                            window,
3357                            cx,
3358                        );
3359                    });
3360                }
3361            },
3362            move |window, cx| {
3363                git_action_tooltip(
3364                    "Publish branch to remote",
3365                    &git::Push {
3366                        options: Some(PushOptions::SetUpstream),
3367                    },
3368                    "git push --set-upstream",
3369                    panel_focus_handle.clone(),
3370                    window,
3371                    cx,
3372                )
3373            },
3374        )
3375    }
3376
3377    fn render_republish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
3378        let panel = self.git_panel.clone();
3379        let panel_focus_handle = self.panel_focus_handle(cx);
3380
3381        SplitButton::new(
3382            id,
3383            "Republish",
3384            0,
3385            0,
3386            Some(IconName::ArrowUpFromLine),
3387            move |_, window, cx| {
3388                if let Some(panel) = panel.as_ref() {
3389                    panel.update(cx, |panel, cx| {
3390                        panel.push(
3391                            &git::Push {
3392                                options: Some(PushOptions::SetUpstream),
3393                            },
3394                            window,
3395                            cx,
3396                        );
3397                    });
3398                }
3399            },
3400            move |window, cx| {
3401                git_action_tooltip(
3402                    "Re-publish branch to remote",
3403                    &git::Push {
3404                        options: Some(PushOptions::SetUpstream),
3405                    },
3406                    "git push --set-upstream",
3407                    panel_focus_handle.clone(),
3408                    window,
3409                    cx,
3410                )
3411            },
3412        )
3413    }
3414
3415    fn render_relevant_button(
3416        &self,
3417        id: impl Into<SharedString>,
3418        branch: &Branch,
3419        cx: &mut App,
3420    ) -> impl IntoElement {
3421        let id = id.into();
3422        let upstream = branch.upstream.as_ref();
3423        match upstream {
3424            Some(Upstream {
3425                tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
3426                ..
3427            }) => match (*ahead, *behind) {
3428                (0, 0) => self.render_fetch_button(id, cx),
3429                (ahead, 0) => self.render_push_button(id, ahead, cx),
3430                (ahead, behind) => self.render_pull_button(id, ahead, behind, cx),
3431            },
3432            Some(Upstream {
3433                tracking: UpstreamTracking::Gone,
3434                ..
3435            }) => self.render_republish_button(id, cx),
3436            None => self.render_publish_button(id, cx),
3437        }
3438    }
3439}
3440
3441impl RenderOnce for PanelRepoFooter {
3442    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
3443        let active_repo = self.active_repository.clone();
3444        let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
3445        let repo_selector_trigger = Button::new("repo-selector", active_repo)
3446            .style(ButtonStyle::Transparent)
3447            .size(ButtonSize::None)
3448            .label_size(LabelSize::Small)
3449            .color(Color::Muted);
3450
3451        let project = self
3452            .git_panel
3453            .as_ref()
3454            .map(|panel| panel.read(cx).project.clone());
3455
3456        let repo = self
3457            .git_panel
3458            .as_ref()
3459            .and_then(|panel| panel.read(cx).active_repository.clone());
3460
3461        let single_repo = project
3462            .as_ref()
3463            .map(|project| project.read(cx).all_repositories(cx).len() == 1)
3464            .unwrap_or(true);
3465
3466        let repo_selector = PopoverMenu::new("repository-switcher")
3467            .menu({
3468                let project = project.clone();
3469                move |window, cx| {
3470                    let project = project.clone()?;
3471                    Some(cx.new(|cx| RepositorySelector::new(project, window, cx)))
3472                }
3473            })
3474            .trigger_with_tooltip(
3475                repo_selector_trigger.disabled(single_repo).truncate(true),
3476                Tooltip::text("Switch active repository"),
3477            )
3478            .attach(gpui::Corner::BottomLeft)
3479            .into_any_element();
3480
3481        let branch = self.branch.clone();
3482        let branch_name = branch
3483            .as_ref()
3484            .map_or(" (no branch)".into(), |branch| branch.name.clone());
3485
3486        let branch_selector_button = Button::new("branch-selector", branch_name)
3487            .style(ButtonStyle::Transparent)
3488            .size(ButtonSize::None)
3489            .label_size(LabelSize::Small)
3490            .truncate(true)
3491            .tooltip(Tooltip::for_action_title(
3492                "Switch Branch",
3493                &zed_actions::git::Branch,
3494            ))
3495            .on_click(|_, window, cx| {
3496                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
3497            });
3498
3499        let branch_selector = PopoverMenu::new("popover-button")
3500            .menu(move |window, cx| Some(branch_picker::popover(repo.clone(), window, cx)))
3501            .trigger_with_tooltip(
3502                branch_selector_button,
3503                Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
3504            )
3505            .anchor(Corner::TopLeft)
3506            .offset(gpui::Point {
3507                x: px(0.0),
3508                y: px(-2.0),
3509            });
3510
3511        let spinner = self
3512            .git_panel
3513            .as_ref()
3514            .and_then(|git_panel| git_panel.read(cx).render_spinner());
3515
3516        h_flex()
3517            .w_full()
3518            .px_2()
3519            .h(px(36.))
3520            .items_center()
3521            .justify_between()
3522            .child(
3523                h_flex()
3524                    .flex_1()
3525                    .overflow_hidden()
3526                    .items_center()
3527                    .child(
3528                        div().child(
3529                            Icon::new(IconName::GitBranchSmall)
3530                                .size(IconSize::Small)
3531                                .color(Color::Muted),
3532                        ),
3533                    )
3534                    .child(repo_selector)
3535                    .when_some(branch.clone(), |this, _| {
3536                        this.child(
3537                            div()
3538                                .text_color(cx.theme().colors().text_muted)
3539                                .text_sm()
3540                                .child("/"),
3541                        )
3542                    })
3543                    .child(branch_selector),
3544            )
3545            .child(
3546                h_flex()
3547                    .gap_1()
3548                    .flex_shrink_0()
3549                    .children(spinner)
3550                    .child(self.render_overflow_menu(overflow_menu_id))
3551                    .when_some(branch, |this, branch| {
3552                        let button = self.render_relevant_button(self.id.clone(), &branch, cx);
3553                        this.child(button)
3554                    }),
3555            )
3556    }
3557}
3558
3559impl ComponentPreview for PanelRepoFooter {
3560    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
3561        let unknown_upstream = None;
3562        let no_remote_upstream = Some(UpstreamTracking::Gone);
3563        let ahead_of_upstream = Some(
3564            UpstreamTrackingStatus {
3565                ahead: 2,
3566                behind: 0,
3567            }
3568            .into(),
3569        );
3570        let behind_upstream = Some(
3571            UpstreamTrackingStatus {
3572                ahead: 0,
3573                behind: 2,
3574            }
3575            .into(),
3576        );
3577        let ahead_and_behind_upstream = Some(
3578            UpstreamTrackingStatus {
3579                ahead: 3,
3580                behind: 1,
3581            }
3582            .into(),
3583        );
3584
3585        let not_ahead_or_behind_upstream = Some(
3586            UpstreamTrackingStatus {
3587                ahead: 0,
3588                behind: 0,
3589            }
3590            .into(),
3591        );
3592
3593        fn branch(upstream: Option<UpstreamTracking>) -> Branch {
3594            Branch {
3595                is_head: true,
3596                name: "some-branch".into(),
3597                upstream: upstream.map(|tracking| Upstream {
3598                    ref_name: "origin/some-branch".into(),
3599                    tracking,
3600                }),
3601                most_recent_commit: Some(CommitSummary {
3602                    sha: "abc123".into(),
3603                    subject: "Modify stuff".into(),
3604                    commit_timestamp: 1710932954,
3605                    has_parent: true,
3606                }),
3607            }
3608        }
3609
3610        fn custom(branch_name: &str, upstream: Option<UpstreamTracking>) -> Branch {
3611            Branch {
3612                is_head: true,
3613                name: branch_name.to_string().into(),
3614                upstream: upstream.map(|tracking| Upstream {
3615                    ref_name: format!("zed/{}", branch_name).into(),
3616                    tracking,
3617                }),
3618                most_recent_commit: Some(CommitSummary {
3619                    sha: "abc123".into(),
3620                    subject: "Modify stuff".into(),
3621                    commit_timestamp: 1710932954,
3622                    has_parent: true,
3623                }),
3624            }
3625        }
3626
3627        fn active_repository(id: usize) -> SharedString {
3628            format!("repo-{}", id).into()
3629        }
3630
3631        let example_width = px(340.);
3632
3633        v_flex()
3634            .gap_6()
3635            .w_full()
3636            .flex_none()
3637            .children(vec![example_group_with_title(
3638                "Action Button States",
3639                vec![
3640                    single_example(
3641                        "No Branch",
3642                        div()
3643                            .w(example_width)
3644                            .overflow_hidden()
3645                            .child(PanelRepoFooter::new_preview(
3646                                "no-branch",
3647                                active_repository(1).clone(),
3648                                None,
3649                            ))
3650                            .into_any_element(),
3651                    )
3652                    .grow(),
3653                    single_example(
3654                        "Remote status unknown",
3655                        div()
3656                            .w(example_width)
3657                            .overflow_hidden()
3658                            .child(PanelRepoFooter::new_preview(
3659                                "unknown-upstream",
3660                                active_repository(2).clone(),
3661                                Some(branch(unknown_upstream)),
3662                            ))
3663                            .into_any_element(),
3664                    )
3665                    .grow(),
3666                    single_example(
3667                        "No Remote Upstream",
3668                        div()
3669                            .w(example_width)
3670                            .overflow_hidden()
3671                            .child(PanelRepoFooter::new_preview(
3672                                "no-remote-upstream",
3673                                active_repository(3).clone(),
3674                                Some(branch(no_remote_upstream)),
3675                            ))
3676                            .into_any_element(),
3677                    )
3678                    .grow(),
3679                    single_example(
3680                        "Not Ahead or Behind",
3681                        div()
3682                            .w(example_width)
3683                            .overflow_hidden()
3684                            .child(PanelRepoFooter::new_preview(
3685                                "not-ahead-or-behind",
3686                                active_repository(4).clone(),
3687                                Some(branch(not_ahead_or_behind_upstream)),
3688                            ))
3689                            .into_any_element(),
3690                    )
3691                    .grow(),
3692                    single_example(
3693                        "Behind remote",
3694                        div()
3695                            .w(example_width)
3696                            .overflow_hidden()
3697                            .child(PanelRepoFooter::new_preview(
3698                                "behind-remote",
3699                                active_repository(5).clone(),
3700                                Some(branch(behind_upstream)),
3701                            ))
3702                            .into_any_element(),
3703                    )
3704                    .grow(),
3705                    single_example(
3706                        "Ahead of remote",
3707                        div()
3708                            .w(example_width)
3709                            .overflow_hidden()
3710                            .child(PanelRepoFooter::new_preview(
3711                                "ahead-of-remote",
3712                                active_repository(6).clone(),
3713                                Some(branch(ahead_of_upstream)),
3714                            ))
3715                            .into_any_element(),
3716                    )
3717                    .grow(),
3718                    single_example(
3719                        "Ahead and behind remote",
3720                        div()
3721                            .w(example_width)
3722                            .overflow_hidden()
3723                            .child(PanelRepoFooter::new_preview(
3724                                "ahead-and-behind",
3725                                active_repository(7).clone(),
3726                                Some(branch(ahead_and_behind_upstream)),
3727                            ))
3728                            .into_any_element(),
3729                    )
3730                    .grow(),
3731                ],
3732            )
3733            .grow()
3734            .vertical()])
3735            .children(vec![example_group_with_title(
3736                "Labels",
3737                vec![
3738                    single_example(
3739                        "Short Branch & Repo",
3740                        div()
3741                            .w(example_width)
3742                            .overflow_hidden()
3743                            .child(PanelRepoFooter::new_preview(
3744                                "short-branch",
3745                                SharedString::from("zed"),
3746                                Some(custom("main", behind_upstream)),
3747                            ))
3748                            .into_any_element(),
3749                    )
3750                    .grow(),
3751                    single_example(
3752                        "Long Branch",
3753                        div()
3754                            .w(example_width)
3755                            .overflow_hidden()
3756                            .child(PanelRepoFooter::new_preview(
3757                                "long-branch",
3758                                SharedString::from("zed"),
3759                                Some(custom(
3760                                    "redesign-and-update-git-ui-list-entry-style",
3761                                    behind_upstream,
3762                                )),
3763                            ))
3764                            .into_any_element(),
3765                    )
3766                    .grow(),
3767                    single_example(
3768                        "Long Repo",
3769                        div()
3770                            .w(example_width)
3771                            .overflow_hidden()
3772                            .child(PanelRepoFooter::new_preview(
3773                                "long-repo",
3774                                SharedString::from("zed-industries-community-examples"),
3775                                Some(custom("gpui", ahead_of_upstream)),
3776                            ))
3777                            .into_any_element(),
3778                    )
3779                    .grow(),
3780                    single_example(
3781                        "Long Repo & Branch",
3782                        div()
3783                            .w(example_width)
3784                            .overflow_hidden()
3785                            .child(PanelRepoFooter::new_preview(
3786                                "long-repo-and-branch",
3787                                SharedString::from("zed-industries-community-examples"),
3788                                Some(custom(
3789                                    "redesign-and-update-git-ui-list-entry-style",
3790                                    behind_upstream,
3791                                )),
3792                            ))
3793                            .into_any_element(),
3794                    )
3795                    .grow(),
3796                    single_example(
3797                        "Uppercase Repo",
3798                        div()
3799                            .w(example_width)
3800                            .overflow_hidden()
3801                            .child(PanelRepoFooter::new_preview(
3802                                "uppercase-repo",
3803                                SharedString::from("LICENSES"),
3804                                Some(custom("main", ahead_of_upstream)),
3805                            ))
3806                            .into_any_element(),
3807                    )
3808                    .grow(),
3809                    single_example(
3810                        "Uppercase Branch",
3811                        div()
3812                            .w(example_width)
3813                            .overflow_hidden()
3814                            .child(PanelRepoFooter::new_preview(
3815                                "uppercase-branch",
3816                                SharedString::from("zed"),
3817                                Some(custom("update-README", behind_upstream)),
3818                            ))
3819                            .into_any_element(),
3820                    )
3821                    .grow(),
3822                ],
3823            )
3824            .grow()
3825            .vertical()])
3826            .into_any_element()
3827    }
3828}