git_panel.rs

   1use crate::askpass_modal::AskPassModal;
   2use crate::commit_modal::CommitModal;
   3use crate::commit_tooltip::CommitTooltip;
   4use crate::commit_view::CommitView;
   5use crate::git_panel_settings::StatusStyle;
   6use crate::project_diff::{self, Diff, ProjectDiff};
   7use crate::remote_output::{self, RemoteAction, SuccessMessage};
   8use crate::{branch_picker, picker_prompt, render_remote_button};
   9use crate::{
  10    git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
  11};
  12use agent_settings::AgentSettings;
  13use anyhow::Context as _;
  14use askpass::AskPassDelegate;
  15use db::kvp::KEY_VALUE_STORE;
  16use editor::{
  17    Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
  18    scroll::ScrollbarAutoHide,
  19};
  20use futures::StreamExt as _;
  21use git::blame::ParsedCommitMessage;
  22use git::repository::{
  23    Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
  24    PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
  25    UpstreamTrackingStatus, get_git_committer,
  26};
  27use git::status::StageStatus;
  28use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
  29use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
  30use gpui::{
  31    Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
  32    DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
  33    ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
  34    PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
  35    WeakEntity, actions, anchored, deferred, percentage, uniform_list,
  36};
  37use itertools::Itertools;
  38use language::{Buffer, File};
  39use language_model::{
  40    ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
  41    LanguageModelRequestMessage, Role,
  42};
  43use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  44use multi_buffer::ExcerptInfo;
  45use notifications::status_toast::{StatusToast, ToastIcon};
  46use panel::{
  47    PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
  48    panel_icon_button,
  49};
  50use project::git_store::{RepositoryEvent, RepositoryId};
  51use project::{
  52    Fs, Project, ProjectPath,
  53    git_store::{GitStoreEvent, Repository},
  54};
  55use serde::{Deserialize, Serialize};
  56use settings::{Settings as _, SettingsStore};
  57use std::future::Future;
  58use std::ops::Range;
  59use std::path::{Path, PathBuf};
  60use std::{collections::HashSet, sync::Arc, time::Duration, usize};
  61use strum::{IntoEnumIterator, VariantNames};
  62use time::OffsetDateTime;
  63use ui::{
  64    Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
  65    Tooltip, prelude::*,
  66};
  67use util::{ResultExt, TryFutureExt, maybe};
  68
  69use workspace::{
  70    Workspace,
  71    dock::{DockPosition, Panel, PanelEvent},
  72    notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
  73};
  74use zed_llm_client::CompletionIntent;
  75
  76actions!(
  77    git_panel,
  78    [
  79        /// Closes the git panel.
  80        Close,
  81        /// Toggles focus on the git panel.
  82        ToggleFocus,
  83        /// Opens the git panel menu.
  84        OpenMenu,
  85        /// Focuses on the commit message editor.
  86        FocusEditor,
  87        /// Focuses on the changes list.
  88        FocusChanges,
  89        /// Toggles automatic co-author suggestions.
  90        ToggleFillCoAuthors,
  91    ]
  92);
  93
  94fn prompt<T>(
  95    msg: &str,
  96    detail: Option<&str>,
  97    window: &mut Window,
  98    cx: &mut App,
  99) -> Task<anyhow::Result<T>>
 100where
 101    T: IntoEnumIterator + VariantNames + 'static,
 102{
 103    let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx);
 104    cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))
 105}
 106
 107#[derive(strum::EnumIter, strum::VariantNames)]
 108#[strum(serialize_all = "title_case")]
 109enum TrashCancel {
 110    Trash,
 111    Cancel,
 112}
 113
 114struct GitMenuState {
 115    has_tracked_changes: bool,
 116    has_staged_changes: bool,
 117    has_unstaged_changes: bool,
 118    has_new_changes: bool,
 119}
 120
 121fn git_panel_context_menu(
 122    focus_handle: FocusHandle,
 123    state: GitMenuState,
 124    window: &mut Window,
 125    cx: &mut App,
 126) -> Entity<ContextMenu> {
 127    ContextMenu::build(window, cx, move |context_menu, _, _| {
 128        context_menu
 129            .context(focus_handle)
 130            .action_disabled_when(
 131                !state.has_unstaged_changes,
 132                "Stage All",
 133                StageAll.boxed_clone(),
 134            )
 135            .action_disabled_when(
 136                !state.has_staged_changes,
 137                "Unstage All",
 138                UnstageAll.boxed_clone(),
 139            )
 140            .separator()
 141            .action("Open Diff", project_diff::Diff.boxed_clone())
 142            .separator()
 143            .action_disabled_when(
 144                !state.has_tracked_changes,
 145                "Discard Tracked Changes",
 146                RestoreTrackedFiles.boxed_clone(),
 147            )
 148            .action_disabled_when(
 149                !state.has_new_changes,
 150                "Trash Untracked Files",
 151                TrashUntrackedFiles.boxed_clone(),
 152            )
 153    })
 154}
 155
 156const GIT_PANEL_KEY: &str = "GitPanel";
 157
 158const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 159
 160pub fn register(workspace: &mut Workspace) {
 161    workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 162        workspace.toggle_panel_focus::<GitPanel>(window, cx);
 163    });
 164    workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {
 165        CommitModal::toggle(workspace, None, window, cx)
 166    });
 167}
 168
 169#[derive(Debug, Clone)]
 170pub enum Event {
 171    Focus,
 172}
 173
 174#[derive(Serialize, Deserialize)]
 175struct SerializedGitPanel {
 176    width: Option<Pixels>,
 177}
 178
 179#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 180enum Section {
 181    Conflict,
 182    Tracked,
 183    New,
 184}
 185
 186#[derive(Debug, PartialEq, Eq, Clone)]
 187struct GitHeaderEntry {
 188    header: Section,
 189}
 190
 191impl GitHeaderEntry {
 192    pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool {
 193        let this = &self.header;
 194        let status = status_entry.status;
 195        match this {
 196            Section::Conflict => {
 197                repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path)
 198            }
 199            Section::Tracked => !status.is_created(),
 200            Section::New => status.is_created(),
 201        }
 202    }
 203    pub fn title(&self) -> &'static str {
 204        match self.header {
 205            Section::Conflict => "Conflicts",
 206            Section::Tracked => "Tracked",
 207            Section::New => "Untracked",
 208        }
 209    }
 210}
 211
 212#[derive(Debug, PartialEq, Eq, Clone)]
 213enum GitListEntry {
 214    Status(GitStatusEntry),
 215    Header(GitHeaderEntry),
 216}
 217
 218impl GitListEntry {
 219    fn status_entry(&self) -> Option<&GitStatusEntry> {
 220        match self {
 221            GitListEntry::Status(entry) => Some(entry),
 222            _ => None,
 223        }
 224    }
 225}
 226
 227#[derive(Debug, PartialEq, Eq, Clone)]
 228pub struct GitStatusEntry {
 229    pub(crate) repo_path: RepoPath,
 230    pub(crate) abs_path: PathBuf,
 231    pub(crate) status: FileStatus,
 232    pub(crate) staging: StageStatus,
 233}
 234
 235impl GitStatusEntry {
 236    fn display_name(&self) -> String {
 237        self.repo_path
 238            .file_name()
 239            .map(|name| name.to_string_lossy().into_owned())
 240            .unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
 241    }
 242
 243    fn parent_dir(&self) -> Option<String> {
 244        self.repo_path
 245            .parent()
 246            .map(|parent| parent.to_string_lossy().into_owned())
 247    }
 248}
 249
 250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 251enum TargetStatus {
 252    Staged,
 253    Unstaged,
 254    Reverted,
 255    Unchanged,
 256}
 257
 258struct PendingOperation {
 259    finished: bool,
 260    target_status: TargetStatus,
 261    entries: Vec<GitStatusEntry>,
 262    op_id: usize,
 263}
 264
 265// computed state related to how to render scrollbars
 266// one per axis
 267// on render we just read this off the panel
 268// we update it when
 269// - settings change
 270// - on focus in, on focus out, on hover, etc.
 271#[derive(Debug)]
 272struct ScrollbarProperties {
 273    axis: Axis,
 274    show_scrollbar: bool,
 275    show_track: bool,
 276    auto_hide: bool,
 277    hide_task: Option<Task<()>>,
 278    state: ScrollbarState,
 279}
 280
 281impl ScrollbarProperties {
 282    // Shows the scrollbar and cancels any pending hide task
 283    fn show(&mut self, cx: &mut Context<GitPanel>) {
 284        if !self.auto_hide {
 285            return;
 286        }
 287        self.show_scrollbar = true;
 288        self.hide_task.take();
 289        cx.notify();
 290    }
 291
 292    fn hide(&mut self, window: &mut Window, cx: &mut Context<GitPanel>) {
 293        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 294
 295        if !self.auto_hide {
 296            return;
 297        }
 298
 299        let axis = self.axis;
 300        self.hide_task = Some(cx.spawn_in(window, async move |panel, cx| {
 301            cx.background_executor()
 302                .timer(SCROLLBAR_SHOW_INTERVAL)
 303                .await;
 304
 305            if let Some(panel) = panel.upgrade() {
 306                panel
 307                    .update(cx, |panel, cx| {
 308                        match axis {
 309                            Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false,
 310                            Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false,
 311                        }
 312                        cx.notify();
 313                    })
 314                    .log_err();
 315            }
 316        }));
 317    }
 318}
 319
 320pub struct GitPanel {
 321    pub(crate) active_repository: Option<Entity<Repository>>,
 322    pub(crate) commit_editor: Entity<Editor>,
 323    conflicted_count: usize,
 324    conflicted_staged_count: usize,
 325    add_coauthors: bool,
 326    generate_commit_message_task: Option<Task<Option<()>>>,
 327    entries: Vec<GitListEntry>,
 328    single_staged_entry: Option<GitStatusEntry>,
 329    single_tracked_entry: Option<GitStatusEntry>,
 330    focus_handle: FocusHandle,
 331    fs: Arc<dyn Fs>,
 332    horizontal_scrollbar: ScrollbarProperties,
 333    vertical_scrollbar: ScrollbarProperties,
 334    new_count: usize,
 335    entry_count: usize,
 336    new_staged_count: usize,
 337    pending: Vec<PendingOperation>,
 338    pending_commit: Option<Task<()>>,
 339    amend_pending: bool,
 340    pending_serialization: Task<Option<()>>,
 341    pub(crate) project: Entity<Project>,
 342    scroll_handle: UniformListScrollHandle,
 343    max_width_item_index: Option<usize>,
 344    selected_entry: Option<usize>,
 345    marked_entries: Vec<usize>,
 346    tracked_count: usize,
 347    tracked_staged_count: usize,
 348    update_visible_entries_task: Task<()>,
 349    width: Option<Pixels>,
 350    workspace: WeakEntity<Workspace>,
 351    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 352    modal_open: bool,
 353    show_placeholders: bool,
 354    local_committer: Option<GitCommitter>,
 355    local_committer_task: Option<Task<()>>,
 356    bulk_staging: Option<BulkStaging>,
 357    _settings_subscription: Subscription,
 358}
 359
 360#[derive(Clone, Debug, PartialEq, Eq)]
 361struct BulkStaging {
 362    repo_id: RepositoryId,
 363    anchor: RepoPath,
 364}
 365
 366const MAX_PANEL_EDITOR_LINES: usize = 6;
 367
 368pub(crate) fn commit_message_editor(
 369    commit_message_buffer: Entity<Buffer>,
 370    placeholder: Option<SharedString>,
 371    project: Entity<Project>,
 372    in_panel: bool,
 373    window: &mut Window,
 374    cx: &mut Context<Editor>,
 375) -> Editor {
 376    let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
 377    let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
 378    let mut commit_editor = Editor::new(
 379        EditorMode::AutoHeight {
 380            min_lines: 1,
 381            max_lines: Some(max_lines),
 382        },
 383        buffer,
 384        None,
 385        window,
 386        cx,
 387    );
 388    commit_editor.set_collaboration_hub(Box::new(project));
 389    commit_editor.set_use_autoclose(false);
 390    commit_editor.set_show_gutter(false, cx);
 391    commit_editor.set_use_modal_editing(true);
 392    commit_editor.set_show_wrap_guides(false, cx);
 393    commit_editor.set_show_indent_guides(false, cx);
 394    let placeholder = placeholder.unwrap_or("Enter commit message".into());
 395    commit_editor.set_placeholder_text(placeholder, cx);
 396    commit_editor
 397}
 398
 399impl GitPanel {
 400    fn new(
 401        workspace: &mut Workspace,
 402        window: &mut Window,
 403        cx: &mut Context<Workspace>,
 404    ) -> Entity<Self> {
 405        let project = workspace.project().clone();
 406        let app_state = workspace.app_state().clone();
 407        let fs = app_state.fs.clone();
 408        let git_store = project.read(cx).git_store().clone();
 409        let active_repository = project.read(cx).active_repository(cx);
 410
 411        let git_panel = cx.new(|cx| {
 412            let focus_handle = cx.focus_handle();
 413            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
 414            cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
 415                this.hide_scrollbars(window, cx);
 416            })
 417            .detach();
 418
 419            let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
 420            cx.observe_global::<SettingsStore>(move |this, cx| {
 421                let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
 422                if is_sort_by_path != was_sort_by_path {
 423                    this.update_visible_entries(cx);
 424                }
 425                was_sort_by_path = is_sort_by_path
 426            })
 427            .detach();
 428
 429            // just to let us render a placeholder editor.
 430            // Once the active git repo is set, this buffer will be replaced.
 431            let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
 432            let commit_editor = cx.new(|cx| {
 433                commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
 434            });
 435
 436            commit_editor.update(cx, |editor, cx| {
 437                editor.clear(window, cx);
 438            });
 439
 440            let scroll_handle = UniformListScrollHandle::new();
 441
 442            let vertical_scrollbar = ScrollbarProperties {
 443                axis: Axis::Vertical,
 444                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 445                show_scrollbar: false,
 446                show_track: false,
 447                auto_hide: false,
 448                hide_task: None,
 449            };
 450
 451            let horizontal_scrollbar = ScrollbarProperties {
 452                axis: Axis::Horizontal,
 453                state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
 454                show_scrollbar: false,
 455                show_track: false,
 456                auto_hide: false,
 457                hide_task: None,
 458            };
 459
 460            let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
 461            let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
 462                if assistant_enabled != AgentSettings::get_global(cx).enabled {
 463                    assistant_enabled = AgentSettings::get_global(cx).enabled;
 464                    cx.notify();
 465                }
 466            });
 467
 468            cx.subscribe_in(
 469                &git_store,
 470                window,
 471                move |this, _git_store, event, window, cx| match event {
 472                    GitStoreEvent::ActiveRepositoryChanged(_) => {
 473                        this.active_repository = this.project.read(cx).active_repository(cx);
 474                        this.schedule_update(true, window, cx);
 475                    }
 476                    GitStoreEvent::RepositoryUpdated(
 477                        _,
 478                        RepositoryEvent::Updated { full_scan, .. },
 479                        true,
 480                    ) => {
 481                        this.schedule_update(*full_scan, window, cx);
 482                    }
 483
 484                    GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
 485                        this.schedule_update(false, window, cx);
 486                    }
 487                    GitStoreEvent::IndexWriteError(error) => {
 488                        this.workspace
 489                            .update(cx, |workspace, cx| {
 490                                workspace.show_error(error, cx);
 491                            })
 492                            .ok();
 493                    }
 494                    GitStoreEvent::RepositoryUpdated(_, _, _) => {}
 495                    GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
 496                },
 497            )
 498            .detach();
 499
 500            let mut this = Self {
 501                active_repository,
 502                commit_editor,
 503                conflicted_count: 0,
 504                conflicted_staged_count: 0,
 505                add_coauthors: true,
 506                generate_commit_message_task: None,
 507                entries: Vec::new(),
 508                focus_handle: cx.focus_handle(),
 509                fs,
 510                new_count: 0,
 511                new_staged_count: 0,
 512                pending: Vec::new(),
 513                pending_commit: None,
 514                amend_pending: false,
 515                pending_serialization: Task::ready(None),
 516                single_staged_entry: None,
 517                single_tracked_entry: None,
 518                project,
 519                scroll_handle,
 520                max_width_item_index: None,
 521                selected_entry: None,
 522                marked_entries: Vec::new(),
 523                tracked_count: 0,
 524                tracked_staged_count: 0,
 525                update_visible_entries_task: Task::ready(()),
 526                width: None,
 527                show_placeholders: false,
 528                local_committer: None,
 529                local_committer_task: None,
 530                context_menu: None,
 531                workspace: workspace.weak_handle(),
 532                modal_open: false,
 533                entry_count: 0,
 534                horizontal_scrollbar,
 535                vertical_scrollbar,
 536                bulk_staging: None,
 537                _settings_subscription,
 538            };
 539
 540            this.schedule_update(false, window, cx);
 541            this
 542        });
 543
 544        git_panel
 545    }
 546
 547    fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 548        self.horizontal_scrollbar.hide(window, cx);
 549        self.vertical_scrollbar.hide(window, cx);
 550    }
 551
 552    fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 553        // TODO: This PR should have defined Editor's `scrollbar.axis`
 554        // as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
 555        // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
 556        //
 557        // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
 558        // so we can show each axis based on the settings.
 559        //
 560        // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
 561
 562        let show_setting = GitPanelSettings::get_global(cx)
 563            .scrollbar
 564            .show
 565            .unwrap_or(EditorSettings::get_global(cx).scrollbar.show);
 566
 567        let scroll_handle = self.scroll_handle.0.borrow();
 568
 569        let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
 570            ShowScrollbar::Auto => true,
 571            ShowScrollbar::System => cx
 572                .try_global::<ScrollbarAutoHide>()
 573                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 574            ShowScrollbar::Always => false,
 575            ShowScrollbar::Never => false,
 576        };
 577
 578        let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
 579            (size.contents.width > size.item.width).then_some(size.contents.width)
 580        });
 581
 582        // is there an item long enough that we should show a horizontal scrollbar?
 583        let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
 584            longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
 585        } else {
 586            true
 587        };
 588
 589        let show_horizontal = match (show_setting, item_wider_than_container) {
 590            (_, false) => false,
 591            (ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true,
 592            (ShowScrollbar::Never, true) => false,
 593        };
 594
 595        let show_vertical = match show_setting {
 596            ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
 597            ShowScrollbar::Never => false,
 598        };
 599
 600        let show_horizontal_track =
 601            show_horizontal && matches!(show_setting, ShowScrollbar::Always);
 602
 603        // TODO: we probably should hide the scroll track when the list doesn't need to scroll
 604        let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
 605
 606        self.vertical_scrollbar = ScrollbarProperties {
 607            axis: self.vertical_scrollbar.axis,
 608            state: self.vertical_scrollbar.state.clone(),
 609            show_scrollbar: show_vertical,
 610            show_track: show_vertical_track,
 611            auto_hide: autohide(show_setting, cx),
 612            hide_task: None,
 613        };
 614
 615        self.horizontal_scrollbar = ScrollbarProperties {
 616            axis: self.horizontal_scrollbar.axis,
 617            state: self.horizontal_scrollbar.state.clone(),
 618            show_scrollbar: show_horizontal,
 619            show_track: show_horizontal_track,
 620            auto_hide: autohide(show_setting, cx),
 621            hide_task: None,
 622        };
 623
 624        cx.notify();
 625    }
 626
 627    pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option<usize> {
 628        if GitPanelSettings::get_global(cx).sort_by_path {
 629            return self
 630                .entries
 631                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
 632                .ok();
 633        }
 634
 635        if self.conflicted_count > 0 {
 636            let conflicted_start = 1;
 637            if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
 638                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
 639            {
 640                return Some(conflicted_start + ix);
 641            }
 642        }
 643        if self.tracked_count > 0 {
 644            let tracked_start = if self.conflicted_count > 0 {
 645                1 + self.conflicted_count
 646            } else {
 647                0
 648            } + 1;
 649            if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
 650                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
 651            {
 652                return Some(tracked_start + ix);
 653            }
 654        }
 655        if self.new_count > 0 {
 656            let untracked_start = if self.conflicted_count > 0 {
 657                1 + self.conflicted_count
 658            } else {
 659                0
 660            } + if self.tracked_count > 0 {
 661                1 + self.tracked_count
 662            } else {
 663                0
 664            } + 1;
 665            if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
 666                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
 667            {
 668                return Some(untracked_start + ix);
 669            }
 670        }
 671        None
 672    }
 673
 674    pub fn select_entry_by_path(
 675        &mut self,
 676        path: ProjectPath,
 677        _: &mut Window,
 678        cx: &mut Context<Self>,
 679    ) {
 680        let Some(git_repo) = self.active_repository.as_ref() else {
 681            return;
 682        };
 683        let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
 684            return;
 685        };
 686        let Some(ix) = self.entry_by_path(&repo_path, cx) else {
 687            return;
 688        };
 689        self.selected_entry = Some(ix);
 690        cx.notify();
 691    }
 692
 693    fn serialize(&mut self, cx: &mut Context<Self>) {
 694        let width = self.width;
 695        self.pending_serialization = cx.background_spawn(
 696            async move {
 697                KEY_VALUE_STORE
 698                    .write_kvp(
 699                        GIT_PANEL_KEY.into(),
 700                        serde_json::to_string(&SerializedGitPanel { width })?,
 701                    )
 702                    .await?;
 703                anyhow::Ok(())
 704            }
 705            .log_err(),
 706        );
 707    }
 708
 709    pub(crate) fn set_modal_open(&mut self, open: bool, cx: &mut Context<Self>) {
 710        self.modal_open = open;
 711        cx.notify();
 712    }
 713
 714    fn dispatch_context(&self, window: &mut Window, cx: &Context<Self>) -> KeyContext {
 715        let mut dispatch_context = KeyContext::new_with_defaults();
 716        dispatch_context.add("GitPanel");
 717
 718        if window
 719            .focused(cx)
 720            .map_or(false, |focused| self.focus_handle == focused)
 721        {
 722            dispatch_context.add("menu");
 723            dispatch_context.add("ChangesList");
 724        }
 725
 726        if self.commit_editor.read(cx).is_focused(window) {
 727            dispatch_context.add("CommitEditor");
 728        }
 729
 730        dispatch_context
 731    }
 732
 733    fn close_panel(&mut self, _: &Close, _window: &mut Window, cx: &mut Context<Self>) {
 734        cx.emit(PanelEvent::Close);
 735    }
 736
 737    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 738        if !self.focus_handle.contains_focused(window, cx) {
 739            cx.emit(Event::Focus);
 740        }
 741    }
 742
 743    fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
 744        if let Some(selected_entry) = self.selected_entry {
 745            self.scroll_handle
 746                .scroll_to_item(selected_entry, ScrollStrategy::Center);
 747        }
 748
 749        cx.notify();
 750    }
 751
 752    fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
 753        if !self.entries.is_empty() {
 754            self.selected_entry = Some(1);
 755            self.scroll_to_selected_entry(cx);
 756        }
 757    }
 758
 759    fn select_previous(
 760        &mut self,
 761        _: &SelectPrevious,
 762        _window: &mut Window,
 763        cx: &mut Context<Self>,
 764    ) {
 765        let item_count = self.entries.len();
 766        if item_count == 0 {
 767            return;
 768        }
 769
 770        if let Some(selected_entry) = self.selected_entry {
 771            let new_selected_entry = if selected_entry > 0 {
 772                selected_entry - 1
 773            } else {
 774                selected_entry
 775            };
 776
 777            if matches!(
 778                self.entries.get(new_selected_entry),
 779                Some(GitListEntry::Header(..))
 780            ) {
 781                if new_selected_entry > 0 {
 782                    self.selected_entry = Some(new_selected_entry - 1)
 783                }
 784            } else {
 785                self.selected_entry = Some(new_selected_entry);
 786            }
 787
 788            self.scroll_to_selected_entry(cx);
 789        }
 790
 791        cx.notify();
 792    }
 793
 794    fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
 795        let item_count = self.entries.len();
 796        if item_count == 0 {
 797            return;
 798        }
 799
 800        if let Some(selected_entry) = self.selected_entry {
 801            let new_selected_entry = if selected_entry < item_count - 1 {
 802                selected_entry + 1
 803            } else {
 804                selected_entry
 805            };
 806            if matches!(
 807                self.entries.get(new_selected_entry),
 808                Some(GitListEntry::Header(..))
 809            ) {
 810                self.selected_entry = Some(new_selected_entry + 1);
 811            } else {
 812                self.selected_entry = Some(new_selected_entry);
 813            }
 814
 815            self.scroll_to_selected_entry(cx);
 816        }
 817
 818        cx.notify();
 819    }
 820
 821    fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
 822        if self.entries.last().is_some() {
 823            self.selected_entry = Some(self.entries.len() - 1);
 824            self.scroll_to_selected_entry(cx);
 825        }
 826    }
 827
 828    fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
 829        self.commit_editor.update(cx, |editor, cx| {
 830            window.focus(&editor.focus_handle(cx));
 831        });
 832        cx.notify();
 833    }
 834
 835    fn select_first_entry_if_none(&mut self, cx: &mut Context<Self>) {
 836        let have_entries = self
 837            .active_repository
 838            .as_ref()
 839            .map_or(false, |active_repository| {
 840                active_repository.read(cx).status_summary().count > 0
 841            });
 842        if have_entries && self.selected_entry.is_none() {
 843            self.selected_entry = Some(1);
 844            self.scroll_to_selected_entry(cx);
 845            cx.notify();
 846        }
 847    }
 848
 849    fn focus_changes_list(
 850        &mut self,
 851        _: &FocusChanges,
 852        window: &mut Window,
 853        cx: &mut Context<Self>,
 854    ) {
 855        self.select_first_entry_if_none(cx);
 856
 857        cx.focus_self(window);
 858        cx.notify();
 859    }
 860
 861    fn get_selected_entry(&self) -> Option<&GitListEntry> {
 862        self.selected_entry.and_then(|i| self.entries.get(i))
 863    }
 864
 865    fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 866        maybe!({
 867            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
 868            let workspace = self.workspace.upgrade()?;
 869            let git_repo = self.active_repository.as_ref()?;
 870
 871            if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
 872                if let Some(project_path) = project_diff.read(cx).active_path(cx) {
 873                    if Some(&entry.repo_path)
 874                        == git_repo
 875                            .read(cx)
 876                            .project_path_to_repo_path(&project_path, cx)
 877                            .as_ref()
 878                    {
 879                        project_diff.focus_handle(cx).focus(window);
 880                        project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
 881                        return None;
 882                    }
 883                }
 884            };
 885
 886            self.workspace
 887                .update(cx, |workspace, cx| {
 888                    ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
 889                })
 890                .ok();
 891            self.focus_handle.focus(window);
 892
 893            Some(())
 894        });
 895    }
 896
 897    fn open_file(
 898        &mut self,
 899        _: &menu::SecondaryConfirm,
 900        window: &mut Window,
 901        cx: &mut Context<Self>,
 902    ) {
 903        maybe!({
 904            let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
 905            let active_repo = self.active_repository.as_ref()?;
 906            let path = active_repo
 907                .read(cx)
 908                .repo_path_to_project_path(&entry.repo_path, cx)?;
 909            if entry.status.is_deleted() {
 910                return None;
 911            }
 912
 913            self.workspace
 914                .update(cx, |workspace, cx| {
 915                    workspace
 916                        .open_path_preview(path, None, false, false, true, window, cx)
 917                        .detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
 918                            Some(format!("{e}"))
 919                        });
 920                })
 921                .ok()
 922        });
 923    }
 924
 925    fn revert_selected(
 926        &mut self,
 927        action: &git::RestoreFile,
 928        window: &mut Window,
 929        cx: &mut Context<Self>,
 930    ) {
 931        maybe!({
 932            let list_entry = self.entries.get(self.selected_entry?)?.clone();
 933            let entry = list_entry.status_entry()?.to_owned();
 934            let skip_prompt = action.skip_prompt || entry.status.is_created();
 935
 936            let prompt = if skip_prompt {
 937                Task::ready(Ok(0))
 938            } else {
 939                let prompt = window.prompt(
 940                    PromptLevel::Warning,
 941                    &format!(
 942                        "Are you sure you want to restore {}?",
 943                        entry
 944                            .repo_path
 945                            .file_name()
 946                            .unwrap_or(entry.repo_path.as_os_str())
 947                            .to_string_lossy()
 948                    ),
 949                    None,
 950                    &["Restore", "Cancel"],
 951                    cx,
 952                );
 953                cx.background_spawn(prompt)
 954            };
 955
 956            let this = cx.weak_entity();
 957            window
 958                .spawn(cx, async move |cx| {
 959                    if prompt.await? != 0 {
 960                        return anyhow::Ok(());
 961                    }
 962
 963                    this.update_in(cx, |this, window, cx| {
 964                        this.revert_entry(&entry, window, cx);
 965                    })?;
 966
 967                    Ok(())
 968                })
 969                .detach();
 970            Some(())
 971        });
 972    }
 973
 974    fn revert_entry(
 975        &mut self,
 976        entry: &GitStatusEntry,
 977        window: &mut Window,
 978        cx: &mut Context<Self>,
 979    ) {
 980        maybe!({
 981            let active_repo = self.active_repository.clone()?;
 982            let path = active_repo
 983                .read(cx)
 984                .repo_path_to_project_path(&entry.repo_path, cx)?;
 985            let workspace = self.workspace.clone();
 986
 987            if entry.status.staging().has_staged() {
 988                self.change_file_stage(false, vec![entry.clone()], cx);
 989            }
 990            let filename = path.path.file_name()?.to_string_lossy();
 991
 992            if !entry.status.is_created() {
 993                self.perform_checkout(vec![entry.clone()], cx);
 994            } else {
 995                let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
 996                cx.spawn_in(window, async move |_, cx| {
 997                    match prompt.await? {
 998                        TrashCancel::Trash => {}
 999                        TrashCancel::Cancel => return Ok(()),
1000                    }
1001                    let task = workspace.update(cx, |workspace, cx| {
1002                        workspace
1003                            .project()
1004                            .update(cx, |project, cx| project.delete_file(path, true, cx))
1005                    })?;
1006                    if let Some(task) = task {
1007                        task.await?;
1008                    }
1009                    Ok(())
1010                })
1011                .detach_and_prompt_err(
1012                    "Failed to trash file",
1013                    window,
1014                    cx,
1015                    |e, _, _| Some(format!("{e}")),
1016                );
1017            }
1018            Some(())
1019        });
1020    }
1021
1022    fn perform_checkout(&mut self, entries: Vec<GitStatusEntry>, cx: &mut Context<Self>) {
1023        let workspace = self.workspace.clone();
1024        let Some(active_repository) = self.active_repository.clone() else {
1025            return;
1026        };
1027
1028        let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
1029        self.pending.push(PendingOperation {
1030            op_id,
1031            target_status: TargetStatus::Reverted,
1032            entries: entries.clone(),
1033            finished: false,
1034        });
1035        self.update_visible_entries(cx);
1036        let task = cx.spawn(async move |_, cx| {
1037            let tasks: Vec<_> = workspace.update(cx, |workspace, cx| {
1038                workspace.project().update(cx, |project, cx| {
1039                    entries
1040                        .iter()
1041                        .filter_map(|entry| {
1042                            let path = active_repository
1043                                .read(cx)
1044                                .repo_path_to_project_path(&entry.repo_path, cx)?;
1045                            Some(project.open_buffer(path, cx))
1046                        })
1047                        .collect()
1048                })
1049            })?;
1050
1051            let buffers = futures::future::join_all(tasks).await;
1052
1053            active_repository
1054                .update(cx, |repo, cx| {
1055                    repo.checkout_files(
1056                        "HEAD",
1057                        entries
1058                            .into_iter()
1059                            .map(|entries| entries.repo_path)
1060                            .collect(),
1061                        cx,
1062                    )
1063                })?
1064                .await??;
1065
1066            let tasks: Vec<_> = cx.update(|cx| {
1067                buffers
1068                    .iter()
1069                    .filter_map(|buffer| {
1070                        buffer.as_ref().ok()?.update(cx, |buffer, cx| {
1071                            buffer.is_dirty().then(|| buffer.reload(cx))
1072                        })
1073                    })
1074                    .collect()
1075            })?;
1076
1077            futures::future::join_all(tasks).await;
1078
1079            Ok(())
1080        });
1081
1082        cx.spawn(async move |this, cx| {
1083            let result = task.await;
1084
1085            this.update(cx, |this, cx| {
1086                for pending in this.pending.iter_mut() {
1087                    if pending.op_id == op_id {
1088                        pending.finished = true;
1089                        if result.is_err() {
1090                            pending.target_status = TargetStatus::Unchanged;
1091                            this.update_visible_entries(cx);
1092                        }
1093                        break;
1094                    }
1095                }
1096                result
1097                    .map_err(|e| {
1098                        this.show_error_toast("checkout", e, cx);
1099                    })
1100                    .ok();
1101            })
1102            .ok();
1103        })
1104        .detach();
1105    }
1106
1107    fn restore_tracked_files(
1108        &mut self,
1109        _: &RestoreTrackedFiles,
1110        window: &mut Window,
1111        cx: &mut Context<Self>,
1112    ) {
1113        let entries = self
1114            .entries
1115            .iter()
1116            .filter_map(|entry| entry.status_entry().cloned())
1117            .filter(|status_entry| !status_entry.status.is_created())
1118            .collect::<Vec<_>>();
1119
1120        match entries.len() {
1121            0 => return,
1122            1 => return self.revert_entry(&entries[0], window, cx),
1123            _ => {}
1124        }
1125        let mut details = entries
1126            .iter()
1127            .filter_map(|entry| entry.repo_path.0.file_name())
1128            .map(|filename| filename.to_string_lossy())
1129            .take(5)
1130            .join("\n");
1131        if entries.len() > 5 {
1132            details.push_str(&format!("\nand {} more…", entries.len() - 5))
1133        }
1134
1135        #[derive(strum::EnumIter, strum::VariantNames)]
1136        #[strum(serialize_all = "title_case")]
1137        enum RestoreCancel {
1138            RestoreTrackedFiles,
1139            Cancel,
1140        }
1141        let prompt = prompt(
1142            "Discard changes to these files?",
1143            Some(&details),
1144            window,
1145            cx,
1146        );
1147        cx.spawn(async move |this, cx| match prompt.await {
1148            Ok(RestoreCancel::RestoreTrackedFiles) => {
1149                this.update(cx, |this, cx| {
1150                    this.perform_checkout(entries, cx);
1151                })
1152                .ok();
1153            }
1154            _ => {
1155                return;
1156            }
1157        })
1158        .detach();
1159    }
1160
1161    fn clean_all(&mut self, _: &TrashUntrackedFiles, window: &mut Window, cx: &mut Context<Self>) {
1162        let workspace = self.workspace.clone();
1163        let Some(active_repo) = self.active_repository.clone() else {
1164            return;
1165        };
1166        let to_delete = self
1167            .entries
1168            .iter()
1169            .filter_map(|entry| entry.status_entry())
1170            .filter(|status_entry| status_entry.status.is_created())
1171            .cloned()
1172            .collect::<Vec<_>>();
1173
1174        match to_delete.len() {
1175            0 => return,
1176            1 => return self.revert_entry(&to_delete[0], window, cx),
1177            _ => {}
1178        };
1179
1180        let mut details = to_delete
1181            .iter()
1182            .map(|entry| {
1183                entry
1184                    .repo_path
1185                    .0
1186                    .file_name()
1187                    .map(|f| f.to_string_lossy())
1188                    .unwrap_or_default()
1189            })
1190            .take(5)
1191            .join("\n");
1192
1193        if to_delete.len() > 5 {
1194            details.push_str(&format!("\nand {} more…", to_delete.len() - 5))
1195        }
1196
1197        let prompt = prompt("Trash these files?", Some(&details), window, cx);
1198        cx.spawn_in(window, async move |this, cx| {
1199            match prompt.await? {
1200                TrashCancel::Trash => {}
1201                TrashCancel::Cancel => return Ok(()),
1202            }
1203            let tasks = workspace.update(cx, |workspace, cx| {
1204                to_delete
1205                    .iter()
1206                    .filter_map(|entry| {
1207                        workspace.project().update(cx, |project, cx| {
1208                            let project_path = active_repo
1209                                .read(cx)
1210                                .repo_path_to_project_path(&entry.repo_path, cx)?;
1211                            project.delete_file(project_path, true, cx)
1212                        })
1213                    })
1214                    .collect::<Vec<_>>()
1215            })?;
1216            let to_unstage = to_delete
1217                .into_iter()
1218                .filter(|entry| !entry.status.staging().is_fully_unstaged())
1219                .collect();
1220            this.update(cx, |this, cx| this.change_file_stage(false, to_unstage, cx))?;
1221            for task in tasks {
1222                task.await?;
1223            }
1224            Ok(())
1225        })
1226        .detach_and_prompt_err("Failed to trash files", window, cx, |e, _, _| {
1227            Some(format!("{e}"))
1228        });
1229    }
1230
1231    pub fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
1232        let entries = self
1233            .entries
1234            .iter()
1235            .filter_map(|entry| entry.status_entry())
1236            .filter(|status_entry| status_entry.staging.has_unstaged())
1237            .cloned()
1238            .collect::<Vec<_>>();
1239        self.change_file_stage(true, entries, cx);
1240    }
1241
1242    pub fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
1243        let entries = self
1244            .entries
1245            .iter()
1246            .filter_map(|entry| entry.status_entry())
1247            .filter(|status_entry| status_entry.staging.has_staged())
1248            .cloned()
1249            .collect::<Vec<_>>();
1250        self.change_file_stage(false, entries, cx);
1251    }
1252
1253    fn toggle_staged_for_entry(
1254        &mut self,
1255        entry: &GitListEntry,
1256        _window: &mut Window,
1257        cx: &mut Context<Self>,
1258    ) {
1259        let Some(active_repository) = self.active_repository.as_ref() else {
1260            return;
1261        };
1262        let (stage, repo_paths) = match entry {
1263            GitListEntry::Status(status_entry) => {
1264                if status_entry.status.staging().is_fully_staged() {
1265                    if let Some(op) = self.bulk_staging.clone()
1266                        && op.anchor == status_entry.repo_path
1267                    {
1268                        self.bulk_staging = None;
1269                    }
1270
1271                    (false, vec![status_entry.clone()])
1272                } else {
1273                    self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
1274
1275                    (true, vec![status_entry.clone()])
1276                }
1277            }
1278            GitListEntry::Header(section) => {
1279                let goal_staged_state = !self.header_state(section.header).selected();
1280                let repository = active_repository.read(cx);
1281                let entries = self
1282                    .entries
1283                    .iter()
1284                    .filter_map(|entry| entry.status_entry())
1285                    .filter(|status_entry| {
1286                        section.contains(&status_entry, repository)
1287                            && status_entry.staging.as_bool() != Some(goal_staged_state)
1288                    })
1289                    .map(|status_entry| status_entry.clone())
1290                    .collect::<Vec<_>>();
1291
1292                (goal_staged_state, entries)
1293            }
1294        };
1295        self.change_file_stage(stage, repo_paths, cx);
1296    }
1297
1298    fn change_file_stage(
1299        &mut self,
1300        stage: bool,
1301        entries: Vec<GitStatusEntry>,
1302        cx: &mut Context<Self>,
1303    ) {
1304        let Some(active_repository) = self.active_repository.clone() else {
1305            return;
1306        };
1307        let op_id = self.pending.iter().map(|p| p.op_id).max().unwrap_or(0) + 1;
1308        self.pending.push(PendingOperation {
1309            op_id,
1310            target_status: if stage {
1311                TargetStatus::Staged
1312            } else {
1313                TargetStatus::Unstaged
1314            },
1315            entries: entries.clone(),
1316            finished: false,
1317        });
1318        let repository = active_repository.read(cx);
1319        self.update_counts(repository);
1320        cx.notify();
1321
1322        cx.spawn({
1323            async move |this, cx| {
1324                let result = cx
1325                    .update(|cx| {
1326                        if stage {
1327                            active_repository.update(cx, |repo, cx| {
1328                                let repo_paths = entries
1329                                    .iter()
1330                                    .map(|entry| entry.repo_path.clone())
1331                                    .collect();
1332                                repo.stage_entries(repo_paths, cx)
1333                            })
1334                        } else {
1335                            active_repository.update(cx, |repo, cx| {
1336                                let repo_paths = entries
1337                                    .iter()
1338                                    .map(|entry| entry.repo_path.clone())
1339                                    .collect();
1340                                repo.unstage_entries(repo_paths, cx)
1341                            })
1342                        }
1343                    })?
1344                    .await;
1345
1346                this.update(cx, |this, cx| {
1347                    for pending in this.pending.iter_mut() {
1348                        if pending.op_id == op_id {
1349                            pending.finished = true
1350                        }
1351                    }
1352                    result
1353                        .map_err(|e| {
1354                            this.show_error_toast(if stage { "add" } else { "reset" }, e, cx);
1355                        })
1356                        .ok();
1357                    cx.notify();
1358                })
1359            }
1360        })
1361        .detach();
1362    }
1363
1364    pub fn total_staged_count(&self) -> usize {
1365        self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
1366    }
1367
1368    pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
1369        self.commit_editor
1370            .read(cx)
1371            .buffer()
1372            .read(cx)
1373            .as_singleton()
1374            .unwrap()
1375            .clone()
1376    }
1377
1378    fn toggle_staged_for_selected(
1379        &mut self,
1380        _: &git::ToggleStaged,
1381        window: &mut Window,
1382        cx: &mut Context<Self>,
1383    ) {
1384        if let Some(selected_entry) = self.get_selected_entry().cloned() {
1385            self.toggle_staged_for_entry(&selected_entry, window, cx);
1386        }
1387    }
1388
1389    fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) {
1390        let Some(index) = self.selected_entry else {
1391            return;
1392        };
1393        self.stage_bulk(index, cx);
1394    }
1395
1396    fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
1397        let Some(selected_entry) = self.get_selected_entry() else {
1398            return;
1399        };
1400        let Some(status_entry) = selected_entry.status_entry() else {
1401            return;
1402        };
1403        if status_entry.staging != StageStatus::Staged {
1404            self.change_file_stage(true, vec![status_entry.clone()], cx);
1405        }
1406    }
1407
1408    fn unstage_selected(
1409        &mut self,
1410        _: &git::UnstageFile,
1411        _window: &mut Window,
1412        cx: &mut Context<Self>,
1413    ) {
1414        let Some(selected_entry) = self.get_selected_entry() else {
1415            return;
1416        };
1417        let Some(status_entry) = selected_entry.status_entry() else {
1418            return;
1419        };
1420        if status_entry.staging != StageStatus::Unstaged {
1421            self.change_file_stage(false, vec![status_entry.clone()], cx);
1422        }
1423    }
1424
1425    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
1426        if self.amend_pending {
1427            return;
1428        }
1429        if self
1430            .commit_editor
1431            .focus_handle(cx)
1432            .contains_focused(window, cx)
1433        {
1434            telemetry::event!("Git Committed", source = "Git Panel");
1435            self.commit_changes(CommitOptions { amend: false }, window, cx)
1436        } else {
1437            cx.propagate();
1438        }
1439    }
1440
1441    fn amend(&mut self, _: &git::Amend, window: &mut Window, cx: &mut Context<Self>) {
1442        if self
1443            .commit_editor
1444            .focus_handle(cx)
1445            .contains_focused(window, cx)
1446        {
1447            if self
1448                .active_repository
1449                .as_ref()
1450                .and_then(|repo| repo.read(cx).head_commit.as_ref())
1451                .is_some()
1452            {
1453                if !self.amend_pending {
1454                    self.set_amend_pending(true, cx);
1455                    self.load_last_commit_message_if_empty(cx);
1456                } else {
1457                    telemetry::event!("Git Amended", source = "Git Panel");
1458                    self.set_amend_pending(false, cx);
1459                    self.commit_changes(CommitOptions { amend: true }, window, cx);
1460                }
1461            }
1462        } else {
1463            cx.propagate();
1464        }
1465    }
1466
1467    pub fn load_last_commit_message_if_empty(&mut self, cx: &mut Context<Self>) {
1468        if !self.commit_editor.read(cx).is_empty(cx) {
1469            return;
1470        }
1471        let Some(active_repository) = self.active_repository.as_ref() else {
1472            return;
1473        };
1474        let Some(recent_sha) = active_repository
1475            .read(cx)
1476            .head_commit
1477            .as_ref()
1478            .map(|commit| commit.sha.to_string())
1479        else {
1480            return;
1481        };
1482        let detail_task = self.load_commit_details(recent_sha, cx);
1483        cx.spawn(async move |this, cx| {
1484            if let Ok(message) = detail_task.await.map(|detail| detail.message) {
1485                this.update(cx, |this, cx| {
1486                    this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1487                        let start = buffer.anchor_before(0);
1488                        let end = buffer.anchor_after(buffer.len());
1489                        buffer.edit([(start..end, message)], None, cx);
1490                    });
1491                })
1492                .log_err();
1493            }
1494        })
1495        .detach();
1496    }
1497
1498    fn cancel(&mut self, _: &git::Cancel, _: &mut Window, cx: &mut Context<Self>) {
1499        if self.amend_pending {
1500            self.set_amend_pending(false, cx);
1501        }
1502    }
1503
1504    fn custom_or_suggested_commit_message(
1505        &self,
1506        window: &mut Window,
1507        cx: &mut Context<Self>,
1508    ) -> Option<String> {
1509        let git_commit_language = self.commit_editor.read(cx).language_at(0, cx);
1510        let message = self.commit_editor.read(cx).text(cx);
1511        if message.is_empty() {
1512            return self
1513                .suggest_commit_message(cx)
1514                .filter(|message| !message.trim().is_empty());
1515        } else if message.trim().is_empty() {
1516            return None;
1517        }
1518        let buffer = cx.new(|cx| {
1519            let mut buffer = Buffer::local(message, cx);
1520            buffer.set_language(git_commit_language, cx);
1521            buffer
1522        });
1523        let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
1524        let wrapped_message = editor.update(cx, |editor, cx| {
1525            editor.select_all(&Default::default(), window, cx);
1526            editor.rewrap(&Default::default(), window, cx);
1527            editor.text(cx)
1528        });
1529        if wrapped_message.trim().is_empty() {
1530            return None;
1531        }
1532        Some(wrapped_message)
1533    }
1534
1535    fn has_commit_message(&self, cx: &mut Context<Self>) -> bool {
1536        let text = self.commit_editor.read(cx).text(cx);
1537        if !text.trim().is_empty() {
1538            return true;
1539        } else if text.is_empty() {
1540            return self
1541                .suggest_commit_message(cx)
1542                .is_some_and(|text| !text.trim().is_empty());
1543        } else {
1544            return false;
1545        }
1546    }
1547
1548    pub(crate) fn commit_changes(
1549        &mut self,
1550        options: CommitOptions,
1551        window: &mut Window,
1552        cx: &mut Context<Self>,
1553    ) {
1554        let Some(active_repository) = self.active_repository.clone() else {
1555            return;
1556        };
1557        let error_spawn = |message, window: &mut Window, cx: &mut App| {
1558            let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
1559            cx.spawn(async move |_| {
1560                prompt.await.ok();
1561            })
1562            .detach();
1563        };
1564
1565        if self.has_unstaged_conflicts() {
1566            error_spawn(
1567                "There are still conflicts. You must stage these before committing",
1568                window,
1569                cx,
1570            );
1571            return;
1572        }
1573
1574        let commit_message = self.custom_or_suggested_commit_message(window, cx);
1575
1576        let Some(mut message) = commit_message else {
1577            self.commit_editor.read(cx).focus_handle(cx).focus(window);
1578            return;
1579        };
1580
1581        if self.add_coauthors {
1582            self.fill_co_authors(&mut message, cx);
1583        }
1584
1585        let task = if self.has_staged_changes() {
1586            // Repository serializes all git operations, so we can just send a commit immediately
1587            let commit_task = active_repository.update(cx, |repo, cx| {
1588                repo.commit(message.into(), None, options, cx)
1589            });
1590            cx.background_spawn(async move { commit_task.await? })
1591        } else {
1592            let changed_files = self
1593                .entries
1594                .iter()
1595                .filter_map(|entry| entry.status_entry())
1596                .filter(|status_entry| !status_entry.status.is_created())
1597                .map(|status_entry| status_entry.repo_path.clone())
1598                .collect::<Vec<_>>();
1599
1600            if changed_files.is_empty() {
1601                error_spawn("No changes to commit", window, cx);
1602                return;
1603            }
1604
1605            let stage_task =
1606                active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx));
1607            cx.spawn(async move |_, cx| {
1608                stage_task.await?;
1609                let commit_task = active_repository.update(cx, |repo, cx| {
1610                    repo.commit(message.into(), None, options, cx)
1611                })?;
1612                commit_task.await?
1613            })
1614        };
1615        let task = cx.spawn_in(window, async move |this, cx| {
1616            let result = task.await;
1617            this.update_in(cx, |this, window, cx| {
1618                this.pending_commit.take();
1619                match result {
1620                    Ok(()) => {
1621                        this.commit_editor
1622                            .update(cx, |editor, cx| editor.clear(window, cx));
1623                    }
1624                    Err(e) => this.show_error_toast("commit", e, cx),
1625                }
1626            })
1627            .ok();
1628        });
1629
1630        self.pending_commit = Some(task);
1631    }
1632
1633    fn uncommit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1634        let Some(repo) = self.active_repository.clone() else {
1635            return;
1636        };
1637        telemetry::event!("Git Uncommitted");
1638
1639        let confirmation = self.check_for_pushed_commits(window, cx);
1640        let prior_head = self.load_commit_details("HEAD".to_string(), cx);
1641
1642        let task = cx.spawn_in(window, async move |this, cx| {
1643            let result = maybe!(async {
1644                if let Ok(true) = confirmation.await {
1645                    let prior_head = prior_head.await?;
1646
1647                    repo.update(cx, |repo, cx| {
1648                        repo.reset("HEAD^".to_string(), ResetMode::Soft, cx)
1649                    })?
1650                    .await??;
1651
1652                    Ok(Some(prior_head))
1653                } else {
1654                    Ok(None)
1655                }
1656            })
1657            .await;
1658
1659            this.update_in(cx, |this, window, cx| {
1660                this.pending_commit.take();
1661                match result {
1662                    Ok(None) => {}
1663                    Ok(Some(prior_commit)) => {
1664                        this.commit_editor.update(cx, |editor, cx| {
1665                            editor.set_text(prior_commit.message, window, cx)
1666                        });
1667                    }
1668                    Err(e) => this.show_error_toast("reset", e, cx),
1669                }
1670            })
1671            .ok();
1672        });
1673
1674        self.pending_commit = Some(task);
1675    }
1676
1677    fn check_for_pushed_commits(
1678        &mut self,
1679        window: &mut Window,
1680        cx: &mut Context<Self>,
1681    ) -> impl Future<Output = anyhow::Result<bool>> + use<> {
1682        let repo = self.active_repository.clone();
1683        let mut cx = window.to_async(cx);
1684
1685        async move {
1686            let repo = repo.context("No active repository")?;
1687
1688            let pushed_to: Vec<SharedString> = repo
1689                .update(&mut cx, |repo, _| repo.check_for_pushed_commits())?
1690                .await??;
1691
1692            if pushed_to.is_empty() {
1693                Ok(true)
1694            } else {
1695                #[derive(strum::EnumIter, strum::VariantNames)]
1696                #[strum(serialize_all = "title_case")]
1697                enum CancelUncommit {
1698                    Uncommit,
1699                    Cancel,
1700                }
1701                let detail = format!(
1702                    "This commit was already pushed to {}.",
1703                    pushed_to.into_iter().join(", ")
1704                );
1705                let result = cx
1706                    .update(|window, cx| prompt("Are you sure?", Some(&detail), window, cx))?
1707                    .await?;
1708
1709                match result {
1710                    CancelUncommit::Cancel => Ok(false),
1711                    CancelUncommit::Uncommit => Ok(true),
1712                }
1713            }
1714        }
1715    }
1716
1717    /// Suggests a commit message based on the changed files and their statuses
1718    pub fn suggest_commit_message(&self, cx: &App) -> Option<String> {
1719        if let Some(merge_message) = self
1720            .active_repository
1721            .as_ref()
1722            .and_then(|repo| repo.read(cx).merge.message.as_ref())
1723        {
1724            return Some(merge_message.to_string());
1725        }
1726
1727        let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
1728            Some(staged_entry)
1729        } else if let Some(single_tracked_entry) = &self.single_tracked_entry {
1730            Some(single_tracked_entry)
1731        } else {
1732            None
1733        }?;
1734
1735        let action_text = if git_status_entry.status.is_deleted() {
1736            Some("Delete")
1737        } else if git_status_entry.status.is_created() {
1738            Some("Create")
1739        } else if git_status_entry.status.is_modified() {
1740            Some("Update")
1741        } else {
1742            None
1743        }?;
1744
1745        let file_name = git_status_entry
1746            .repo_path
1747            .file_name()
1748            .unwrap_or_default()
1749            .to_string_lossy();
1750
1751        Some(format!("{} {}", action_text, file_name))
1752    }
1753
1754    fn generate_commit_message_action(
1755        &mut self,
1756        _: &git::GenerateCommitMessage,
1757        _window: &mut Window,
1758        cx: &mut Context<Self>,
1759    ) {
1760        self.generate_commit_message(cx);
1761    }
1762
1763    /// Generates a commit message using an LLM.
1764    pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
1765        if !self.can_commit() {
1766            return;
1767        }
1768
1769        let model = match current_language_model(cx) {
1770            Some(value) => value,
1771            None => return,
1772        };
1773
1774        let Some(repo) = self.active_repository.as_ref() else {
1775            return;
1776        };
1777
1778        telemetry::event!("Git Commit Message Generated");
1779
1780        let diff = repo.update(cx, |repo, cx| {
1781            if self.has_staged_changes() {
1782                repo.diff(DiffType::HeadToIndex, cx)
1783            } else {
1784                repo.diff(DiffType::HeadToWorktree, cx)
1785            }
1786        });
1787
1788        let temperature = AgentSettings::temperature_for_model(&model, cx);
1789
1790        self.generate_commit_message_task = Some(cx.spawn(async move |this, cx| {
1791             async move {
1792                let _defer = cx.on_drop(&this, |this, _cx| {
1793                    this.generate_commit_message_task.take();
1794                });
1795
1796                let mut diff_text = match diff.await {
1797                    Ok(result) => match result {
1798                        Ok(text) => text,
1799                        Err(e) => {
1800                            Self::show_commit_message_error(&this, &e, cx);
1801                            return anyhow::Ok(());
1802                        }
1803                    },
1804                    Err(e) => {
1805                        Self::show_commit_message_error(&this, &e, cx);
1806                        return anyhow::Ok(());
1807                    }
1808                };
1809
1810                const ONE_MB: usize = 1_000_000;
1811                if diff_text.len() > ONE_MB {
1812                    diff_text = diff_text.chars().take(ONE_MB).collect()
1813                }
1814
1815                let subject = this.update(cx, |this, cx| {
1816                    this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
1817                })?;
1818
1819                let text_empty = subject.trim().is_empty();
1820
1821                let content = if text_empty {
1822                    format!("{PROMPT}\nHere are the changes in this commit:\n{diff_text}")
1823                } else {
1824                    format!("{PROMPT}\nHere is the user's subject line:\n{subject}\nHere are the changes in this commit:\n{diff_text}\n")
1825                };
1826
1827                const PROMPT: &str = include_str!("commit_message_prompt.txt");
1828
1829                let request = LanguageModelRequest {
1830                    thread_id: None,
1831                    prompt_id: None,
1832                    intent: Some(CompletionIntent::GenerateGitCommitMessage),
1833                    mode: None,
1834                    messages: vec![LanguageModelRequestMessage {
1835                        role: Role::User,
1836                        content: vec![content.into()],
1837                        cache: false,
1838                    }],
1839                    tools: Vec::new(),
1840                    tool_choice: None,
1841                    stop: Vec::new(),
1842                    temperature,
1843                    thinking_allowed: false,
1844                };
1845
1846                let stream = model.stream_completion_text(request, &cx);
1847                match stream.await {
1848                    Ok(mut messages) => {
1849                        if !text_empty {
1850                            this.update(cx, |this, cx| {
1851                                this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1852                                    let insert_position = buffer.anchor_before(buffer.len());
1853                                    buffer.edit([(insert_position..insert_position, "\n")], None, cx)
1854                                });
1855                            })?;
1856                        }
1857
1858                        while let Some(message) = messages.stream.next().await {
1859                            match message {
1860                                Ok(text) => {
1861                                    this.update(cx, |this, cx| {
1862                                        this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1863                                            let insert_position = buffer.anchor_before(buffer.len());
1864                                            buffer.edit([(insert_position..insert_position, text)], None, cx);
1865                                        });
1866                                    })?;
1867                                }
1868                                Err(e) => {
1869                                    Self::show_commit_message_error(&this, &e, cx);
1870                                    break;
1871                                }
1872                            }
1873                        }
1874                    }
1875                    Err(e) => {
1876                        Self::show_commit_message_error(&this, &e, cx);
1877                    }
1878                }
1879
1880                anyhow::Ok(())
1881            }
1882            .log_err().await
1883        }));
1884    }
1885
1886    fn get_fetch_options(
1887        &self,
1888        window: &mut Window,
1889        cx: &mut Context<Self>,
1890    ) -> Task<Option<FetchOptions>> {
1891        let repo = self.active_repository.clone();
1892        let workspace = self.workspace.clone();
1893
1894        cx.spawn_in(window, async move |_, cx| {
1895            let repo = repo?;
1896            let remotes = repo
1897                .update(cx, |repo, _| repo.get_remotes(None))
1898                .ok()?
1899                .await
1900                .ok()?
1901                .log_err()?;
1902
1903            let mut remotes: Vec<_> = remotes.into_iter().map(FetchOptions::Remote).collect();
1904            if remotes.len() > 1 {
1905                remotes.push(FetchOptions::All);
1906            }
1907            let selection = cx
1908                .update(|window, cx| {
1909                    picker_prompt::prompt(
1910                        "Pick which remote to fetch",
1911                        remotes.iter().map(|r| r.name()).collect(),
1912                        workspace,
1913                        window,
1914                        cx,
1915                    )
1916                })
1917                .ok()?
1918                .await?;
1919            remotes.get(selection).cloned()
1920        })
1921    }
1922
1923    pub(crate) fn fetch(
1924        &mut self,
1925        is_fetch_all: bool,
1926        window: &mut Window,
1927        cx: &mut Context<Self>,
1928    ) {
1929        if !self.can_push_and_pull(cx) {
1930            return;
1931        }
1932
1933        let Some(repo) = self.active_repository.clone() else {
1934            return;
1935        };
1936        telemetry::event!("Git Fetched");
1937        let askpass = self.askpass_delegate("git fetch", window, cx);
1938        let this = cx.weak_entity();
1939
1940        let fetch_options = if is_fetch_all {
1941            Task::ready(Some(FetchOptions::All))
1942        } else {
1943            self.get_fetch_options(window, cx)
1944        };
1945
1946        window
1947            .spawn(cx, async move |cx| {
1948                let Some(fetch_options) = fetch_options.await else {
1949                    return Ok(());
1950                };
1951                let fetch = repo.update(cx, |repo, cx| {
1952                    repo.fetch(fetch_options.clone(), askpass, cx)
1953                })?;
1954
1955                let remote_message = fetch.await?;
1956                this.update(cx, |this, cx| {
1957                    let action = match fetch_options {
1958                        FetchOptions::All => RemoteAction::Fetch(None),
1959                        FetchOptions::Remote(remote) => RemoteAction::Fetch(Some(remote)),
1960                    };
1961                    match remote_message {
1962                        Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
1963                        Err(e) => {
1964                            log::error!("Error while fetching {:?}", e);
1965                            this.show_error_toast(action.name(), e, cx)
1966                        }
1967                    }
1968
1969                    anyhow::Ok(())
1970                })
1971                .ok();
1972                anyhow::Ok(())
1973            })
1974            .detach_and_log_err(cx);
1975    }
1976
1977    pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1978        let worktrees = self
1979            .project
1980            .read(cx)
1981            .visible_worktrees(cx)
1982            .collect::<Vec<_>>();
1983
1984        let worktree = if worktrees.len() == 1 {
1985            Task::ready(Some(worktrees.first().unwrap().clone()))
1986        } else if worktrees.len() == 0 {
1987            let result = window.prompt(
1988                PromptLevel::Warning,
1989                "Unable to initialize a git repository",
1990                Some("Open a directory first"),
1991                &["Ok"],
1992                cx,
1993            );
1994            cx.background_executor()
1995                .spawn(async move {
1996                    result.await.ok();
1997                })
1998                .detach();
1999            return;
2000        } else {
2001            let worktree_directories = worktrees
2002                .iter()
2003                .map(|worktree| worktree.read(cx).abs_path())
2004                .map(|worktree_abs_path| {
2005                    if let Ok(path) = worktree_abs_path.strip_prefix(util::paths::home_dir()) {
2006                        Path::new("~")
2007                            .join(path)
2008                            .to_string_lossy()
2009                            .to_string()
2010                            .into()
2011                    } else {
2012                        worktree_abs_path.to_string_lossy().to_string().into()
2013                    }
2014                })
2015                .collect_vec();
2016            let prompt = picker_prompt::prompt(
2017                "Where would you like to initialize this git repository?",
2018                worktree_directories,
2019                self.workspace.clone(),
2020                window,
2021                cx,
2022            );
2023
2024            cx.spawn(async move |_, _| prompt.await.map(|ix| worktrees[ix].clone()))
2025        };
2026
2027        cx.spawn_in(window, async move |this, cx| {
2028            let worktree = match worktree.await {
2029                Some(worktree) => worktree,
2030                None => {
2031                    return;
2032                }
2033            };
2034
2035            let Ok(result) = this.update(cx, |this, cx| {
2036                let fallback_branch_name = GitPanelSettings::get_global(cx)
2037                    .fallback_branch_name
2038                    .clone();
2039                this.project.read(cx).git_init(
2040                    worktree.read(cx).abs_path(),
2041                    fallback_branch_name,
2042                    cx,
2043                )
2044            }) else {
2045                return;
2046            };
2047
2048            let result = result.await;
2049
2050            this.update_in(cx, |this, _, cx| match result {
2051                Ok(()) => {}
2052                Err(e) => this.show_error_toast("init", e, cx),
2053            })
2054            .ok();
2055        })
2056        .detach();
2057    }
2058
2059    pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2060        if !self.can_push_and_pull(cx) {
2061            return;
2062        }
2063        let Some(repo) = self.active_repository.clone() else {
2064            return;
2065        };
2066        let Some(branch) = repo.read(cx).branch.as_ref() else {
2067            return;
2068        };
2069        telemetry::event!("Git Pulled");
2070        let branch = branch.clone();
2071        let remote = self.get_remote(false, window, cx);
2072        cx.spawn_in(window, async move |this, cx| {
2073            let remote = match remote.await {
2074                Ok(Some(remote)) => remote,
2075                Ok(None) => {
2076                    return Ok(());
2077                }
2078                Err(e) => {
2079                    log::error!("Failed to get current remote: {}", e);
2080                    this.update(cx, |this, cx| this.show_error_toast("pull", e, cx))
2081                        .ok();
2082                    return Ok(());
2083                }
2084            };
2085
2086            let askpass = this.update_in(cx, |this, window, cx| {
2087                this.askpass_delegate(format!("git pull {}", remote.name), window, cx)
2088            })?;
2089
2090            let pull = repo.update(cx, |repo, cx| {
2091                repo.pull(
2092                    branch.name().to_owned().into(),
2093                    remote.name.clone(),
2094                    askpass,
2095                    cx,
2096                )
2097            })?;
2098
2099            let remote_message = pull.await?;
2100
2101            let action = RemoteAction::Pull(remote);
2102            this.update(cx, |this, cx| match remote_message {
2103                Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
2104                Err(e) => {
2105                    log::error!("Error while pulling {:?}", e);
2106                    this.show_error_toast(action.name(), e, cx)
2107                }
2108            })
2109            .ok();
2110
2111            anyhow::Ok(())
2112        })
2113        .detach_and_log_err(cx);
2114    }
2115
2116    pub(crate) fn push(
2117        &mut self,
2118        force_push: bool,
2119        select_remote: bool,
2120        window: &mut Window,
2121        cx: &mut Context<Self>,
2122    ) {
2123        if !self.can_push_and_pull(cx) {
2124            return;
2125        }
2126        let Some(repo) = self.active_repository.clone() else {
2127            return;
2128        };
2129        let Some(branch) = repo.read(cx).branch.as_ref() else {
2130            return;
2131        };
2132        telemetry::event!("Git Pushed");
2133        let branch = branch.clone();
2134
2135        let options = if force_push {
2136            Some(PushOptions::Force)
2137        } else {
2138            match branch.upstream {
2139                Some(Upstream {
2140                    tracking: UpstreamTracking::Gone,
2141                    ..
2142                })
2143                | None => Some(PushOptions::SetUpstream),
2144                _ => None,
2145            }
2146        };
2147        let remote = self.get_remote(select_remote, window, cx);
2148
2149        cx.spawn_in(window, async move |this, cx| {
2150            let remote = match remote.await {
2151                Ok(Some(remote)) => remote,
2152                Ok(None) => {
2153                    return Ok(());
2154                }
2155                Err(e) => {
2156                    log::error!("Failed to get current remote: {}", e);
2157                    this.update(cx, |this, cx| this.show_error_toast("push", e, cx))
2158                        .ok();
2159                    return Ok(());
2160                }
2161            };
2162
2163            let askpass_delegate = this.update_in(cx, |this, window, cx| {
2164                this.askpass_delegate(format!("git push {}", remote.name), window, cx)
2165            })?;
2166
2167            let push = repo.update(cx, |repo, cx| {
2168                repo.push(
2169                    branch.name().to_owned().into(),
2170                    remote.name.clone(),
2171                    options,
2172                    askpass_delegate,
2173                    cx,
2174                )
2175            })?;
2176
2177            let remote_output = push.await?;
2178
2179            let action = RemoteAction::Push(branch.name().to_owned().into(), remote);
2180            this.update(cx, |this, cx| match remote_output {
2181                Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
2182                Err(e) => {
2183                    log::error!("Error while pushing {:?}", e);
2184                    this.show_error_toast(action.name(), e, cx)
2185                }
2186            })?;
2187
2188            anyhow::Ok(())
2189        })
2190        .detach_and_log_err(cx);
2191    }
2192
2193    fn askpass_delegate(
2194        &self,
2195        operation: impl Into<SharedString>,
2196        window: &mut Window,
2197        cx: &mut Context<Self>,
2198    ) -> AskPassDelegate {
2199        let this = cx.weak_entity();
2200        let operation = operation.into();
2201        let window = window.window_handle();
2202        AskPassDelegate::new(&mut cx.to_async(), move |prompt, tx, cx| {
2203            window
2204                .update(cx, |_, window, cx| {
2205                    this.update(cx, |this, cx| {
2206                        this.workspace.update(cx, |workspace, cx| {
2207                            workspace.toggle_modal(window, cx, |window, cx| {
2208                                AskPassModal::new(operation.clone(), prompt.into(), tx, window, cx)
2209                            });
2210                        })
2211                    })
2212                })
2213                .ok();
2214        })
2215    }
2216
2217    fn can_push_and_pull(&self, cx: &App) -> bool {
2218        !self.project.read(cx).is_via_collab()
2219    }
2220
2221    fn get_remote(
2222        &mut self,
2223        always_select: bool,
2224        window: &mut Window,
2225        cx: &mut Context<Self>,
2226    ) -> impl Future<Output = anyhow::Result<Option<Remote>>> + use<> {
2227        let repo = self.active_repository.clone();
2228        let workspace = self.workspace.clone();
2229        let mut cx = window.to_async(cx);
2230
2231        async move {
2232            let repo = repo.context("No active repository")?;
2233            let current_remotes: Vec<Remote> = repo
2234                .update(&mut cx, |repo, _| {
2235                    let current_branch = if always_select {
2236                        None
2237                    } else {
2238                        let current_branch = repo.branch.as_ref().context("No active branch")?;
2239                        Some(current_branch.name().to_string())
2240                    };
2241                    anyhow::Ok(repo.get_remotes(current_branch))
2242                })??
2243                .await??;
2244
2245            let current_remotes: Vec<_> = current_remotes
2246                .into_iter()
2247                .map(|remotes| remotes.name)
2248                .collect();
2249            let selection = cx
2250                .update(|window, cx| {
2251                    picker_prompt::prompt(
2252                        "Pick which remote to push to",
2253                        current_remotes.clone(),
2254                        workspace,
2255                        window,
2256                        cx,
2257                    )
2258                })?
2259                .await;
2260
2261            Ok(selection.map(|selection| Remote {
2262                name: current_remotes[selection].clone(),
2263            }))
2264        }
2265    }
2266
2267    pub fn load_local_committer(&mut self, cx: &Context<Self>) {
2268        if self.local_committer_task.is_none() {
2269            self.local_committer_task = Some(cx.spawn(async move |this, cx| {
2270                let committer = get_git_committer(cx).await;
2271                this.update(cx, |this, cx| {
2272                    this.local_committer = Some(committer);
2273                    cx.notify()
2274                })
2275                .ok();
2276            }));
2277        }
2278    }
2279
2280    fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
2281        let mut new_co_authors = Vec::new();
2282        let project = self.project.read(cx);
2283
2284        let Some(room) = self
2285            .workspace
2286            .upgrade()
2287            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned())
2288        else {
2289            return Vec::default();
2290        };
2291
2292        let room = room.read(cx);
2293
2294        for (peer_id, collaborator) in project.collaborators() {
2295            if collaborator.is_host {
2296                continue;
2297            }
2298
2299            let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
2300                continue;
2301            };
2302            if !participant.can_write() {
2303                continue;
2304            }
2305            if let Some(email) = &collaborator.committer_email {
2306                let name = collaborator
2307                    .committer_name
2308                    .clone()
2309                    .or_else(|| participant.user.name.clone())
2310                    .unwrap_or_else(|| participant.user.github_login.clone());
2311                new_co_authors.push((name.clone(), email.clone()))
2312            }
2313        }
2314        if !project.is_local() && !project.is_read_only(cx) {
2315            if let Some(local_committer) = self.local_committer(room, cx) {
2316                new_co_authors.push(local_committer);
2317            }
2318        }
2319        new_co_authors
2320    }
2321
2322    fn local_committer(&self, room: &call::Room, cx: &App) -> Option<(String, String)> {
2323        let user = room.local_participant_user(cx)?;
2324        let committer = self.local_committer.as_ref()?;
2325        let email = committer.email.clone()?;
2326        let name = committer
2327            .name
2328            .clone()
2329            .or_else(|| user.name.clone())
2330            .unwrap_or_else(|| user.github_login.clone());
2331        Some((name, email))
2332    }
2333
2334    fn toggle_fill_co_authors(
2335        &mut self,
2336        _: &ToggleFillCoAuthors,
2337        _: &mut Window,
2338        cx: &mut Context<Self>,
2339    ) {
2340        self.add_coauthors = !self.add_coauthors;
2341        cx.notify();
2342    }
2343
2344    fn fill_co_authors(&mut self, message: &mut String, cx: &mut Context<Self>) {
2345        const CO_AUTHOR_PREFIX: &str = "Co-authored-by: ";
2346
2347        let existing_text = message.to_ascii_lowercase();
2348        let lowercase_co_author_prefix = CO_AUTHOR_PREFIX.to_lowercase();
2349        let mut ends_with_co_authors = false;
2350        let existing_co_authors = existing_text
2351            .lines()
2352            .filter_map(|line| {
2353                let line = line.trim();
2354                if line.starts_with(&lowercase_co_author_prefix) {
2355                    ends_with_co_authors = true;
2356                    Some(line)
2357                } else {
2358                    ends_with_co_authors = false;
2359                    None
2360                }
2361            })
2362            .collect::<HashSet<_>>();
2363
2364        let new_co_authors = self
2365            .potential_co_authors(cx)
2366            .into_iter()
2367            .filter(|(_, email)| {
2368                !existing_co_authors
2369                    .iter()
2370                    .any(|existing| existing.contains(email.as_str()))
2371            })
2372            .collect::<Vec<_>>();
2373
2374        if new_co_authors.is_empty() {
2375            return;
2376        }
2377
2378        if !ends_with_co_authors {
2379            message.push('\n');
2380        }
2381        for (name, email) in new_co_authors {
2382            message.push('\n');
2383            message.push_str(CO_AUTHOR_PREFIX);
2384            message.push_str(&name);
2385            message.push_str(" <");
2386            message.push_str(&email);
2387            message.push('>');
2388        }
2389        message.push('\n');
2390    }
2391
2392    fn schedule_update(
2393        &mut self,
2394        clear_pending: bool,
2395        window: &mut Window,
2396        cx: &mut Context<Self>,
2397    ) {
2398        let handle = cx.entity().downgrade();
2399        self.reopen_commit_buffer(window, cx);
2400        self.update_visible_entries_task = cx.spawn_in(window, async move |_, cx| {
2401            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
2402            if let Some(git_panel) = handle.upgrade() {
2403                git_panel
2404                    .update_in(cx, |git_panel, window, cx| {
2405                        if clear_pending {
2406                            git_panel.clear_pending();
2407                        }
2408                        git_panel.update_visible_entries(cx);
2409                        git_panel.update_scrollbar_properties(window, cx);
2410                    })
2411                    .ok();
2412            }
2413        });
2414    }
2415
2416    fn reopen_commit_buffer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2417        let Some(active_repo) = self.active_repository.as_ref() else {
2418            return;
2419        };
2420        let load_buffer = active_repo.update(cx, |active_repo, cx| {
2421            let project = self.project.read(cx);
2422            active_repo.open_commit_buffer(
2423                Some(project.languages().clone()),
2424                project.buffer_store().clone(),
2425                cx,
2426            )
2427        });
2428
2429        cx.spawn_in(window, async move |git_panel, cx| {
2430            let buffer = load_buffer.await?;
2431            git_panel.update_in(cx, |git_panel, window, cx| {
2432                if git_panel
2433                    .commit_editor
2434                    .read(cx)
2435                    .buffer()
2436                    .read(cx)
2437                    .as_singleton()
2438                    .as_ref()
2439                    != Some(&buffer)
2440                {
2441                    git_panel.commit_editor = cx.new(|cx| {
2442                        commit_message_editor(
2443                            buffer,
2444                            git_panel.suggest_commit_message(cx).map(SharedString::from),
2445                            git_panel.project.clone(),
2446                            true,
2447                            window,
2448                            cx,
2449                        )
2450                    });
2451                }
2452            })
2453        })
2454        .detach_and_log_err(cx);
2455    }
2456
2457    fn clear_pending(&mut self) {
2458        self.pending.retain(|v| !v.finished)
2459    }
2460
2461    fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
2462        let bulk_staging = self.bulk_staging.take();
2463        let last_staged_path_prev_index = bulk_staging
2464            .as_ref()
2465            .and_then(|op| self.entry_by_path(&op.anchor, cx));
2466
2467        self.entries.clear();
2468        self.single_staged_entry.take();
2469        self.single_tracked_entry.take();
2470        self.conflicted_count = 0;
2471        self.conflicted_staged_count = 0;
2472        self.new_count = 0;
2473        self.tracked_count = 0;
2474        self.new_staged_count = 0;
2475        self.tracked_staged_count = 0;
2476        self.entry_count = 0;
2477
2478        let sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
2479
2480        let mut changed_entries = Vec::new();
2481        let mut new_entries = Vec::new();
2482        let mut conflict_entries = Vec::new();
2483        let mut single_staged_entry = None;
2484        let mut staged_count = 0;
2485        let mut max_width_item: Option<(RepoPath, usize)> = None;
2486
2487        let Some(repo) = self.active_repository.as_ref() else {
2488            // Just clear entries if no repository is active.
2489            cx.notify();
2490            return;
2491        };
2492
2493        let repo = repo.read(cx);
2494
2495        for entry in repo.cached_status() {
2496            let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path);
2497            let is_new = entry.status.is_created();
2498            let staging = entry.status.staging();
2499
2500            if self.pending.iter().any(|pending| {
2501                pending.target_status == TargetStatus::Reverted
2502                    && !pending.finished
2503                    && pending
2504                        .entries
2505                        .iter()
2506                        .any(|pending| pending.repo_path == entry.repo_path)
2507            }) {
2508                continue;
2509            }
2510
2511            let abs_path = repo.work_directory_abs_path.join(&entry.repo_path.0);
2512            let entry = GitStatusEntry {
2513                repo_path: entry.repo_path.clone(),
2514                abs_path,
2515                status: entry.status,
2516                staging,
2517            };
2518
2519            if staging.has_staged() {
2520                staged_count += 1;
2521                single_staged_entry = Some(entry.clone());
2522            }
2523
2524            let width_estimate = Self::item_width_estimate(
2525                entry.parent_dir().map(|s| s.len()).unwrap_or(0),
2526                entry.display_name().len(),
2527            );
2528
2529            match max_width_item.as_mut() {
2530                Some((repo_path, estimate)) => {
2531                    if width_estimate > *estimate {
2532                        *repo_path = entry.repo_path.clone();
2533                        *estimate = width_estimate;
2534                    }
2535                }
2536                None => max_width_item = Some((entry.repo_path.clone(), width_estimate)),
2537            }
2538
2539            if sort_by_path {
2540                changed_entries.push(entry);
2541            } else if is_conflict {
2542                conflict_entries.push(entry);
2543            } else if is_new {
2544                new_entries.push(entry);
2545            } else {
2546                changed_entries.push(entry);
2547            }
2548        }
2549
2550        let mut pending_staged_count = 0;
2551        let mut last_pending_staged = None;
2552        let mut pending_status_for_single_staged = None;
2553        for pending in self.pending.iter() {
2554            if pending.target_status == TargetStatus::Staged {
2555                pending_staged_count += pending.entries.len();
2556                last_pending_staged = pending.entries.iter().next().cloned();
2557            }
2558            if let Some(single_staged) = &single_staged_entry {
2559                if pending
2560                    .entries
2561                    .iter()
2562                    .any(|entry| entry.repo_path == single_staged.repo_path)
2563                {
2564                    pending_status_for_single_staged = Some(pending.target_status);
2565                }
2566            }
2567        }
2568
2569        if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
2570            match pending_status_for_single_staged {
2571                Some(TargetStatus::Staged) | None => {
2572                    self.single_staged_entry = single_staged_entry;
2573                }
2574                _ => {}
2575            }
2576        } else if conflict_entries.len() == 0 && pending_staged_count == 1 {
2577            self.single_staged_entry = last_pending_staged;
2578        }
2579
2580        if conflict_entries.len() == 0 && changed_entries.len() == 1 {
2581            self.single_tracked_entry = changed_entries.first().cloned();
2582        }
2583
2584        if conflict_entries.len() > 0 {
2585            self.entries.push(GitListEntry::Header(GitHeaderEntry {
2586                header: Section::Conflict,
2587            }));
2588            self.entries
2589                .extend(conflict_entries.into_iter().map(GitListEntry::Status));
2590        }
2591
2592        if changed_entries.len() > 0 {
2593            if !sort_by_path {
2594                self.entries.push(GitListEntry::Header(GitHeaderEntry {
2595                    header: Section::Tracked,
2596                }));
2597            }
2598            self.entries
2599                .extend(changed_entries.into_iter().map(GitListEntry::Status));
2600        }
2601        if new_entries.len() > 0 {
2602            self.entries.push(GitListEntry::Header(GitHeaderEntry {
2603                header: Section::New,
2604            }));
2605            self.entries
2606                .extend(new_entries.into_iter().map(GitListEntry::Status));
2607        }
2608
2609        if let Some((repo_path, _)) = max_width_item {
2610            self.max_width_item_index = self.entries.iter().position(|entry| match entry {
2611                GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path,
2612                GitListEntry::Header(_) => false,
2613            });
2614        }
2615
2616        self.update_counts(repo);
2617
2618        let bulk_staging_anchor_new_index = bulk_staging
2619            .as_ref()
2620            .filter(|op| op.repo_id == repo.id)
2621            .and_then(|op| self.entry_by_path(&op.anchor, cx));
2622        if bulk_staging_anchor_new_index == last_staged_path_prev_index
2623            && let Some(index) = bulk_staging_anchor_new_index
2624            && let Some(entry) = self.entries.get(index)
2625            && let Some(entry) = entry.status_entry()
2626            && self.entry_staging(entry) == StageStatus::Staged
2627        {
2628            self.bulk_staging = bulk_staging;
2629        }
2630
2631        self.select_first_entry_if_none(cx);
2632
2633        let suggested_commit_message = self.suggest_commit_message(cx);
2634        let placeholder_text = suggested_commit_message.unwrap_or("Enter commit message".into());
2635
2636        self.commit_editor.update(cx, |editor, cx| {
2637            editor.set_placeholder_text(Arc::from(placeholder_text), cx)
2638        });
2639
2640        cx.notify();
2641    }
2642
2643    fn header_state(&self, header_type: Section) -> ToggleState {
2644        let (staged_count, count) = match header_type {
2645            Section::New => (self.new_staged_count, self.new_count),
2646            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
2647            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
2648        };
2649        if staged_count == 0 {
2650            ToggleState::Unselected
2651        } else if count == staged_count {
2652            ToggleState::Selected
2653        } else {
2654            ToggleState::Indeterminate
2655        }
2656    }
2657
2658    fn update_counts(&mut self, repo: &Repository) {
2659        self.show_placeholders = false;
2660        self.conflicted_count = 0;
2661        self.conflicted_staged_count = 0;
2662        self.new_count = 0;
2663        self.tracked_count = 0;
2664        self.new_staged_count = 0;
2665        self.tracked_staged_count = 0;
2666        self.entry_count = 0;
2667        for entry in &self.entries {
2668            let Some(status_entry) = entry.status_entry() else {
2669                continue;
2670            };
2671            self.entry_count += 1;
2672            if repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) {
2673                self.conflicted_count += 1;
2674                if self.entry_staging(status_entry).has_staged() {
2675                    self.conflicted_staged_count += 1;
2676                }
2677            } else if status_entry.status.is_created() {
2678                self.new_count += 1;
2679                if self.entry_staging(status_entry).has_staged() {
2680                    self.new_staged_count += 1;
2681                }
2682            } else {
2683                self.tracked_count += 1;
2684                if self.entry_staging(status_entry).has_staged() {
2685                    self.tracked_staged_count += 1;
2686                }
2687            }
2688        }
2689    }
2690
2691    fn entry_staging(&self, entry: &GitStatusEntry) -> StageStatus {
2692        for pending in self.pending.iter().rev() {
2693            if pending
2694                .entries
2695                .iter()
2696                .any(|pending_entry| pending_entry.repo_path == entry.repo_path)
2697            {
2698                match pending.target_status {
2699                    TargetStatus::Staged => return StageStatus::Staged,
2700                    TargetStatus::Unstaged => return StageStatus::Unstaged,
2701                    TargetStatus::Reverted => continue,
2702                    TargetStatus::Unchanged => continue,
2703                }
2704            }
2705        }
2706        entry.staging
2707    }
2708
2709    pub(crate) fn has_staged_changes(&self) -> bool {
2710        self.tracked_staged_count > 0
2711            || self.new_staged_count > 0
2712            || self.conflicted_staged_count > 0
2713    }
2714
2715    pub(crate) fn has_unstaged_changes(&self) -> bool {
2716        self.tracked_count > self.tracked_staged_count
2717            || self.new_count > self.new_staged_count
2718            || self.conflicted_count > self.conflicted_staged_count
2719    }
2720
2721    fn has_tracked_changes(&self) -> bool {
2722        self.tracked_count > 0
2723    }
2724
2725    pub fn has_unstaged_conflicts(&self) -> bool {
2726        self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
2727    }
2728
2729    fn show_error_toast(&self, action: impl Into<SharedString>, e: anyhow::Error, cx: &mut App) {
2730        let action = action.into();
2731        let Some(workspace) = self.workspace.upgrade() else {
2732            return;
2733        };
2734
2735        let message = e.to_string().trim().to_string();
2736        if message
2737            .matches(git::repository::REMOTE_CANCELLED_BY_USER)
2738            .next()
2739            .is_some()
2740        {
2741            return; // Hide the cancelled by user message
2742        } else {
2743            workspace.update(cx, |workspace, cx| {
2744                let workspace_weak = cx.weak_entity();
2745                let toast = StatusToast::new(format!("git {} failed", action), cx, |this, _cx| {
2746                    this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
2747                        .action("View Log", move |window, cx| {
2748                            let message = message.clone();
2749                            let action = action.clone();
2750                            workspace_weak
2751                                .update(cx, move |workspace, cx| {
2752                                    Self::open_output(action, workspace, &message, window, cx)
2753                                })
2754                                .ok();
2755                        })
2756                });
2757                workspace.toggle_status_toast(toast, cx)
2758            });
2759        }
2760    }
2761
2762    fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp)
2763    where
2764        E: std::fmt::Debug + std::fmt::Display,
2765    {
2766        if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
2767            let _ = workspace.update(cx, |workspace, cx| {
2768                struct CommitMessageError;
2769                let notification_id = NotificationId::unique::<CommitMessageError>();
2770                workspace.show_notification(notification_id, cx, |cx| {
2771                    cx.new(|cx| {
2772                        ErrorMessagePrompt::new(
2773                            format!("Failed to generate commit message: {err}"),
2774                            cx,
2775                        )
2776                    })
2777                });
2778            });
2779        }
2780    }
2781
2782    fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
2783        let Some(workspace) = self.workspace.upgrade() else {
2784            return;
2785        };
2786
2787        workspace.update(cx, |workspace, cx| {
2788            let SuccessMessage { message, style } = remote_output::format_output(&action, info);
2789            let workspace_weak = cx.weak_entity();
2790            let operation = action.name();
2791
2792            let status_toast = StatusToast::new(message, cx, move |this, _cx| {
2793                use remote_output::SuccessStyle::*;
2794                match style {
2795                    Toast { .. } => this,
2796                    ToastWithLog { output } => this
2797                        .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
2798                        .action("View Log", move |window, cx| {
2799                            let output = output.clone();
2800                            let output =
2801                                format!("stdout:\n{}\nstderr:\n{}", output.stdout, output.stderr);
2802                            workspace_weak
2803                                .update(cx, move |workspace, cx| {
2804                                    Self::open_output(operation, workspace, &output, window, cx)
2805                                })
2806                                .ok();
2807                        }),
2808                    PushPrLink { link } => this
2809                        .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
2810                        .action("Open Pull Request", move |_, cx| cx.open_url(&link)),
2811                }
2812            });
2813            workspace.toggle_status_toast(status_toast, cx)
2814        });
2815    }
2816
2817    fn open_output(
2818        operation: impl Into<SharedString>,
2819        workspace: &mut Workspace,
2820        output: &str,
2821        window: &mut Window,
2822        cx: &mut Context<Workspace>,
2823    ) {
2824        let operation = operation.into();
2825        let buffer = cx.new(|cx| Buffer::local(output, cx));
2826        buffer.update(cx, |buffer, cx| {
2827            buffer.set_capability(language::Capability::ReadOnly, cx);
2828        });
2829        let editor = cx.new(|cx| {
2830            let mut editor = Editor::for_buffer(buffer, None, window, cx);
2831            editor.buffer().update(cx, |buffer, cx| {
2832                buffer.set_title(format!("Output from git {operation}"), cx);
2833            });
2834            editor.set_read_only(true);
2835            editor
2836        });
2837
2838        workspace.add_item_to_center(Box::new(editor), window, cx);
2839    }
2840
2841    pub fn can_commit(&self) -> bool {
2842        (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts()
2843    }
2844
2845    pub fn can_stage_all(&self) -> bool {
2846        self.has_unstaged_changes()
2847    }
2848
2849    pub fn can_unstage_all(&self) -> bool {
2850        self.has_staged_changes()
2851    }
2852
2853    // eventually we'll need to take depth into account here
2854    // if we add a tree view
2855    fn item_width_estimate(path: usize, file_name: usize) -> usize {
2856        path + file_name
2857    }
2858
2859    fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
2860        let focus_handle = self.focus_handle.clone();
2861        let has_tracked_changes = self.has_tracked_changes();
2862        let has_staged_changes = self.has_staged_changes();
2863        let has_unstaged_changes = self.has_unstaged_changes();
2864        let has_new_changes = self.new_count > 0;
2865
2866        PopoverMenu::new(id.into())
2867            .trigger(
2868                IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
2869                    .icon_size(IconSize::Small)
2870                    .icon_color(Color::Muted),
2871            )
2872            .menu(move |window, cx| {
2873                Some(git_panel_context_menu(
2874                    focus_handle.clone(),
2875                    GitMenuState {
2876                        has_tracked_changes,
2877                        has_staged_changes,
2878                        has_unstaged_changes,
2879                        has_new_changes,
2880                    },
2881                    window,
2882                    cx,
2883                ))
2884            })
2885            .anchor(Corner::TopRight)
2886    }
2887
2888    pub(crate) fn render_generate_commit_message_button(
2889        &self,
2890        cx: &Context<Self>,
2891    ) -> Option<AnyElement> {
2892        current_language_model(cx).is_some().then(|| {
2893            if self.generate_commit_message_task.is_some() {
2894                return h_flex()
2895                    .gap_1()
2896                    .child(
2897                        Icon::new(IconName::ArrowCircle)
2898                            .size(IconSize::XSmall)
2899                            .color(Color::Info)
2900                            .with_animation(
2901                                "arrow-circle",
2902                                Animation::new(Duration::from_secs(2)).repeat(),
2903                                |icon, delta| {
2904                                    icon.transform(Transformation::rotate(percentage(delta)))
2905                                },
2906                            ),
2907                    )
2908                    .child(
2909                        Label::new("Generating Commit...")
2910                            .size(LabelSize::Small)
2911                            .color(Color::Muted),
2912                    )
2913                    .into_any_element();
2914            }
2915
2916            let can_commit = self.can_commit();
2917            let editor_focus_handle = self.commit_editor.focus_handle(cx);
2918            IconButton::new("generate-commit-message", IconName::AiEdit)
2919                .shape(ui::IconButtonShape::Square)
2920                .icon_color(Color::Muted)
2921                .tooltip(move |window, cx| {
2922                    if can_commit {
2923                        Tooltip::for_action_in(
2924                            "Generate Commit Message",
2925                            &git::GenerateCommitMessage,
2926                            &editor_focus_handle,
2927                            window,
2928                            cx,
2929                        )
2930                    } else {
2931                        Tooltip::simple("No changes to commit", cx)
2932                    }
2933                })
2934                .disabled(!can_commit)
2935                .on_click(cx.listener(move |this, _event, _window, cx| {
2936                    this.generate_commit_message(cx);
2937                }))
2938                .into_any_element()
2939        })
2940    }
2941
2942    pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
2943        let potential_co_authors = self.potential_co_authors(cx);
2944
2945        let (tooltip_label, icon) = if self.add_coauthors {
2946            ("Remove co-authored-by", IconName::Person)
2947        } else {
2948            ("Add co-authored-by", IconName::UserCheck)
2949        };
2950
2951        if potential_co_authors.is_empty() {
2952            None
2953        } else {
2954            Some(
2955                IconButton::new("co-authors", icon)
2956                    .shape(ui::IconButtonShape::Square)
2957                    .icon_color(Color::Disabled)
2958                    .selected_icon_color(Color::Selected)
2959                    .toggle_state(self.add_coauthors)
2960                    .tooltip(move |_, cx| {
2961                        let title = format!(
2962                            "{}:{}{}",
2963                            tooltip_label,
2964                            if potential_co_authors.len() == 1 {
2965                                ""
2966                            } else {
2967                                "\n"
2968                            },
2969                            potential_co_authors
2970                                .iter()
2971                                .map(|(name, email)| format!(" {} <{}>", name, email))
2972                                .join("\n")
2973                        );
2974                        Tooltip::simple(title, cx)
2975                    })
2976                    .on_click(cx.listener(|this, _, _, cx| {
2977                        this.add_coauthors = !this.add_coauthors;
2978                        cx.notify();
2979                    }))
2980                    .into_any_element(),
2981            )
2982        }
2983    }
2984
2985    fn render_git_commit_menu(
2986        &self,
2987        id: impl Into<ElementId>,
2988        keybinding_target: Option<FocusHandle>,
2989        cx: &mut Context<Self>,
2990    ) -> impl IntoElement {
2991        PopoverMenu::new(id.into())
2992            .trigger(
2993                ui::ButtonLike::new_rounded_right("commit-split-button-right")
2994                    .layer(ui::ElevationIndex::ModalSurface)
2995                    .size(ButtonSize::None)
2996                    .child(
2997                        h_flex()
2998                            .px_1()
2999                            .h_full()
3000                            .justify_center()
3001                            .border_l_1()
3002                            .border_color(cx.theme().colors().border)
3003                            .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
3004                    ),
3005            )
3006            .menu(move |window, cx| {
3007                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
3008                    context_menu
3009                        .when_some(keybinding_target.clone(), |el, keybinding_target| {
3010                            el.context(keybinding_target.clone())
3011                        })
3012                        .action("Amend", Amend.boxed_clone())
3013                }))
3014            })
3015            .anchor(Corner::TopRight)
3016    }
3017
3018    pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
3019        if self.has_unstaged_conflicts() {
3020            (false, "You must resolve conflicts before committing")
3021        } else if !self.has_staged_changes() && !self.has_tracked_changes() {
3022            (false, "No changes to commit")
3023        } else if self.pending_commit.is_some() {
3024            (false, "Commit in progress")
3025        } else if !self.has_commit_message(cx) {
3026            (false, "No commit message")
3027        } else if !self.has_write_access(cx) {
3028            (false, "You do not have write access to this project")
3029        } else {
3030            (true, self.commit_button_title())
3031        }
3032    }
3033
3034    pub fn commit_button_title(&self) -> &'static str {
3035        if self.amend_pending {
3036            if self.has_staged_changes() {
3037                "Amend"
3038            } else {
3039                "Amend Tracked"
3040            }
3041        } else {
3042            if self.has_staged_changes() {
3043                "Commit"
3044            } else {
3045                "Commit Tracked"
3046            }
3047        }
3048    }
3049
3050    fn expand_commit_editor(
3051        &mut self,
3052        _: &git::ExpandCommitEditor,
3053        window: &mut Window,
3054        cx: &mut Context<Self>,
3055    ) {
3056        let workspace = self.workspace.clone();
3057        window.defer(cx, move |window, cx| {
3058            workspace
3059                .update(cx, |workspace, cx| {
3060                    CommitModal::toggle(workspace, None, window, cx)
3061                })
3062                .ok();
3063        })
3064    }
3065
3066    fn render_panel_header(
3067        &self,
3068        window: &mut Window,
3069        cx: &mut Context<Self>,
3070    ) -> Option<impl IntoElement> {
3071        self.active_repository.as_ref()?;
3072
3073        let text;
3074        let action;
3075        let tooltip;
3076        if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
3077            text = "Unstage All";
3078            action = git::UnstageAll.boxed_clone();
3079            tooltip = "git reset";
3080        } else {
3081            text = "Stage All";
3082            action = git::StageAll.boxed_clone();
3083            tooltip = "git add --all ."
3084        }
3085
3086        let change_string = match self.entry_count {
3087            0 => "No Changes".to_string(),
3088            1 => "1 Change".to_string(),
3089            _ => format!("{} Changes", self.entry_count),
3090        };
3091
3092        Some(
3093            self.panel_header_container(window, cx)
3094                .px_2()
3095                .justify_between()
3096                .child(
3097                    panel_button(change_string)
3098                        .color(Color::Muted)
3099                        .tooltip(Tooltip::for_action_title_in(
3100                            "Open Diff",
3101                            &Diff,
3102                            &self.focus_handle,
3103                        ))
3104                        .on_click(|_, _, cx| {
3105                            cx.defer(|cx| {
3106                                cx.dispatch_action(&Diff);
3107                            })
3108                        }),
3109                )
3110                .child(
3111                    h_flex()
3112                        .gap_1()
3113                        .child(self.render_overflow_menu("overflow_menu"))
3114                        .child(
3115                            panel_filled_button(text)
3116                                .tooltip(Tooltip::for_action_title_in(
3117                                    tooltip,
3118                                    action.as_ref(),
3119                                    &self.focus_handle,
3120                                ))
3121                                .disabled(self.entry_count == 0)
3122                                .on_click(move |_, _, cx| {
3123                                    let action = action.boxed_clone();
3124                                    cx.defer(move |cx| {
3125                                        cx.dispatch_action(action.as_ref());
3126                                    })
3127                                }),
3128                        ),
3129                ),
3130        )
3131    }
3132
3133    pub(crate) fn render_remote_button(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3134        let branch = self.active_repository.as_ref()?.read(cx).branch.clone();
3135        if !self.can_push_and_pull(cx) {
3136            return None;
3137        }
3138        Some(
3139            h_flex()
3140                .gap_1()
3141                .flex_shrink_0()
3142                .when_some(branch, |this, branch| {
3143                    let focus_handle = Some(self.focus_handle(cx));
3144
3145                    this.children(render_remote_button(
3146                        "remote-button",
3147                        &branch,
3148                        focus_handle,
3149                        true,
3150                    ))
3151                })
3152                .into_any_element(),
3153        )
3154    }
3155
3156    pub fn render_footer(
3157        &self,
3158        window: &mut Window,
3159        cx: &mut Context<Self>,
3160    ) -> Option<impl IntoElement> {
3161        let active_repository = self.active_repository.clone()?;
3162        let panel_editor_style = panel_editor_style(true, window, cx);
3163
3164        let enable_coauthors = self.render_co_authors(cx);
3165
3166        let editor_focus_handle = self.commit_editor.focus_handle(cx);
3167        let expand_tooltip_focus_handle = editor_focus_handle.clone();
3168
3169        let branch = active_repository.read(cx).branch.clone();
3170        let head_commit = active_repository.read(cx).head_commit.clone();
3171
3172        let footer_size = px(32.);
3173        let gap = px(9.0);
3174        let max_height = panel_editor_style
3175            .text
3176            .line_height_in_pixels(window.rem_size())
3177            * MAX_PANEL_EDITOR_LINES
3178            + gap;
3179
3180        let git_panel = cx.entity().clone();
3181        let display_name = SharedString::from(Arc::from(
3182            active_repository
3183                .read(cx)
3184                .display_name()
3185                .trim_end_matches("/"),
3186        ));
3187        let editor_is_long = self.commit_editor.update(cx, |editor, cx| {
3188            editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32
3189        });
3190        let has_previous_commit = head_commit.is_some();
3191
3192        let footer = v_flex()
3193            .child(PanelRepoFooter::new(
3194                display_name,
3195                branch,
3196                head_commit,
3197                Some(git_panel.clone()),
3198            ))
3199            .child(
3200                panel_editor_container(window, cx)
3201                    .id("commit-editor-container")
3202                    .relative()
3203                    .w_full()
3204                    .h(max_height + footer_size)
3205                    .border_t_1()
3206                    .border_color(cx.theme().colors().border)
3207                    .cursor_text()
3208                    .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
3209                        window.focus(&this.commit_editor.focus_handle(cx));
3210                    }))
3211                    .child(
3212                        h_flex()
3213                            .id("commit-footer")
3214                            .border_t_1()
3215                            .when(editor_is_long, |el| {
3216                                el.border_color(cx.theme().colors().border_variant)
3217                            })
3218                            .absolute()
3219                            .bottom_0()
3220                            .left_0()
3221                            .w_full()
3222                            .px_2()
3223                            .h(footer_size)
3224                            .flex_none()
3225                            .justify_between()
3226                            .child(
3227                                self.render_generate_commit_message_button(cx)
3228                                    .unwrap_or_else(|| div().into_any_element()),
3229                            )
3230                            .child(
3231                                h_flex()
3232                                    .gap_0p5()
3233                                    .children(enable_coauthors)
3234                                    .child(self.render_commit_button(has_previous_commit, cx)),
3235                            ),
3236                    )
3237                    .child(
3238                        div()
3239                            .pr_2p5()
3240                            .on_action(|&editor::actions::MoveUp, _, cx| {
3241                                cx.stop_propagation();
3242                            })
3243                            .on_action(|&editor::actions::MoveDown, _, cx| {
3244                                cx.stop_propagation();
3245                            })
3246                            .child(EditorElement::new(&self.commit_editor, panel_editor_style)),
3247                    )
3248                    .child(
3249                        h_flex()
3250                            .absolute()
3251                            .top_2()
3252                            .right_2()
3253                            .opacity(0.5)
3254                            .hover(|this| this.opacity(1.0))
3255                            .child(
3256                                panel_icon_button("expand-commit-editor", IconName::Maximize)
3257                                    .icon_size(IconSize::Small)
3258                                    .size(ui::ButtonSize::Default)
3259                                    .tooltip(move |window, cx| {
3260                                        Tooltip::for_action_in(
3261                                            "Open Commit Modal",
3262                                            &git::ExpandCommitEditor,
3263                                            &expand_tooltip_focus_handle,
3264                                            window,
3265                                            cx,
3266                                        )
3267                                    })
3268                                    .on_click(cx.listener({
3269                                        move |_, _, window, cx| {
3270                                            window.dispatch_action(
3271                                                git::ExpandCommitEditor.boxed_clone(),
3272                                                cx,
3273                                            )
3274                                        }
3275                                    })),
3276                            ),
3277                    ),
3278            );
3279
3280        Some(footer)
3281    }
3282
3283    fn render_commit_button(
3284        &self,
3285        has_previous_commit: bool,
3286        cx: &mut Context<Self>,
3287    ) -> impl IntoElement {
3288        let (can_commit, tooltip) = self.configure_commit_button(cx);
3289        let title = self.commit_button_title();
3290        let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
3291
3292        div()
3293            .id("commit-wrapper")
3294            .on_hover(cx.listener(move |this, hovered, _, cx| {
3295                this.show_placeholders =
3296                    *hovered && !this.has_staged_changes() && !this.has_unstaged_conflicts();
3297                cx.notify()
3298            }))
3299            .when(self.amend_pending, {
3300                |this| {
3301                    this.h_flex()
3302                        .gap_1()
3303                        .child(
3304                            panel_filled_button("Cancel")
3305                                .tooltip({
3306                                    let handle = commit_tooltip_focus_handle.clone();
3307                                    move |window, cx| {
3308                                        Tooltip::for_action_in(
3309                                            "Cancel amend",
3310                                            &git::Cancel,
3311                                            &handle,
3312                                            window,
3313                                            cx,
3314                                        )
3315                                    }
3316                                })
3317                                .on_click(move |_, window, cx| {
3318                                    window.dispatch_action(Box::new(git::Cancel), cx);
3319                                }),
3320                        )
3321                        .child(
3322                            panel_filled_button(title)
3323                                .tooltip({
3324                                    let handle = commit_tooltip_focus_handle.clone();
3325                                    move |window, cx| {
3326                                        if can_commit {
3327                                            Tooltip::for_action_in(
3328                                                tooltip, &Amend, &handle, window, cx,
3329                                            )
3330                                        } else {
3331                                            Tooltip::simple(tooltip, cx)
3332                                        }
3333                                    }
3334                                })
3335                                .disabled(!can_commit || self.modal_open)
3336                                .on_click({
3337                                    let git_panel = cx.weak_entity();
3338                                    move |_, window, cx| {
3339                                        telemetry::event!("Git Amended", source = "Git Panel");
3340                                        git_panel
3341                                            .update(cx, |git_panel, cx| {
3342                                                git_panel.set_amend_pending(false, cx);
3343                                                git_panel.commit_changes(
3344                                                    CommitOptions { amend: true },
3345                                                    window,
3346                                                    cx,
3347                                                );
3348                                            })
3349                                            .ok();
3350                                    }
3351                                }),
3352                        )
3353                }
3354            })
3355            .when(!self.amend_pending, |this| {
3356                this.when(has_previous_commit, |this| {
3357                    this.child(SplitButton::new(
3358                        ui::ButtonLike::new_rounded_left(ElementId::Name(
3359                            format!("split-button-left-{}", title).into(),
3360                        ))
3361                        .layer(ui::ElevationIndex::ModalSurface)
3362                        .size(ui::ButtonSize::Compact)
3363                        .child(
3364                            div()
3365                                .child(Label::new(title).size(LabelSize::Small))
3366                                .mr_0p5(),
3367                        )
3368                        .on_click({
3369                            let git_panel = cx.weak_entity();
3370                            move |_, window, cx| {
3371                                telemetry::event!("Git Committed", source = "Git Panel");
3372                                git_panel
3373                                    .update(cx, |git_panel, cx| {
3374                                        git_panel.commit_changes(
3375                                            CommitOptions { amend: false },
3376                                            window,
3377                                            cx,
3378                                        );
3379                                    })
3380                                    .ok();
3381                            }
3382                        })
3383                        .disabled(!can_commit || self.modal_open)
3384                        .tooltip({
3385                            let handle = commit_tooltip_focus_handle.clone();
3386                            move |window, cx| {
3387                                if can_commit {
3388                                    Tooltip::with_meta_in(
3389                                        tooltip,
3390                                        Some(&git::Commit),
3391                                        "git commit",
3392                                        &handle.clone(),
3393                                        window,
3394                                        cx,
3395                                    )
3396                                } else {
3397                                    Tooltip::simple(tooltip, cx)
3398                                }
3399                            }
3400                        }),
3401                        self.render_git_commit_menu(
3402                            ElementId::Name(format!("split-button-right-{}", title).into()),
3403                            Some(commit_tooltip_focus_handle.clone()),
3404                            cx,
3405                        )
3406                        .into_any_element(),
3407                    ))
3408                })
3409                .when(!has_previous_commit, |this| {
3410                    this.child(
3411                        panel_filled_button(title)
3412                            .tooltip(move |window, cx| {
3413                                if can_commit {
3414                                    Tooltip::with_meta_in(
3415                                        tooltip,
3416                                        Some(&git::Commit),
3417                                        "git commit",
3418                                        &commit_tooltip_focus_handle,
3419                                        window,
3420                                        cx,
3421                                    )
3422                                } else {
3423                                    Tooltip::simple(tooltip, cx)
3424                                }
3425                            })
3426                            .disabled(!can_commit || self.modal_open)
3427                            .on_click({
3428                                let git_panel = cx.weak_entity();
3429                                move |_, window, cx| {
3430                                    telemetry::event!("Git Committed", source = "Git Panel");
3431                                    git_panel
3432                                        .update(cx, |git_panel, cx| {
3433                                            git_panel.commit_changes(
3434                                                CommitOptions { amend: false },
3435                                                window,
3436                                                cx,
3437                                            );
3438                                        })
3439                                        .ok();
3440                                }
3441                            }),
3442                    )
3443                })
3444            })
3445    }
3446
3447    fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
3448        div()
3449            .p_2()
3450            .border_t_1()
3451            .border_color(cx.theme().colors().border)
3452            .child(
3453                Label::new(
3454                    "This will update your most recent commit. Cancel to make a new one instead.",
3455                )
3456                .size(LabelSize::Small),
3457            )
3458    }
3459
3460    fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
3461        let active_repository = self.active_repository.as_ref()?;
3462        let branch = active_repository.read(cx).branch.as_ref()?;
3463        let commit = branch.most_recent_commit.as_ref()?.clone();
3464        let workspace = self.workspace.clone();
3465        let this = cx.entity();
3466
3467        Some(
3468            h_flex()
3469                .py_1p5()
3470                .px_2()
3471                .gap_1p5()
3472                .justify_between()
3473                .border_t_1()
3474                .border_color(cx.theme().colors().border.opacity(0.8))
3475                .child(
3476                    div()
3477                        .flex_grow()
3478                        .overflow_hidden()
3479                        .max_w(relative(0.85))
3480                        .child(
3481                            Label::new(commit.subject.clone())
3482                                .size(LabelSize::Small)
3483                                .truncate(),
3484                        )
3485                        .id("commit-msg-hover")
3486                        .on_click({
3487                            let commit = commit.clone();
3488                            let repo = active_repository.downgrade();
3489                            move |_, window, cx| {
3490                                CommitView::open(
3491                                    commit.clone(),
3492                                    repo.clone(),
3493                                    workspace.clone().clone(),
3494                                    window,
3495                                    cx,
3496                                );
3497                            }
3498                        })
3499                        .hoverable_tooltip({
3500                            let repo = active_repository.clone();
3501                            move |window, cx| {
3502                                GitPanelMessageTooltip::new(
3503                                    this.clone(),
3504                                    commit.sha.clone(),
3505                                    repo.clone(),
3506                                    window,
3507                                    cx,
3508                                )
3509                                .into()
3510                            }
3511                        }),
3512                )
3513                .when(commit.has_parent, |this| {
3514                    let has_unstaged = self.has_unstaged_changes();
3515                    this.child(
3516                        panel_icon_button("undo", IconName::Undo)
3517                            .icon_size(IconSize::XSmall)
3518                            .icon_color(Color::Muted)
3519                            .tooltip(move |window, cx| {
3520                                Tooltip::with_meta(
3521                                    "Uncommit",
3522                                    Some(&git::Uncommit),
3523                                    if has_unstaged {
3524                                        "git reset HEAD^ --soft"
3525                                    } else {
3526                                        "git reset HEAD^"
3527                                    },
3528                                    window,
3529                                    cx,
3530                                )
3531                            })
3532                            .on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
3533                    )
3534                }),
3535        )
3536    }
3537
3538    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
3539        h_flex().h_full().flex_grow().justify_center().child(
3540            v_flex()
3541                .gap_2()
3542                .child(h_flex().w_full().justify_around().child(
3543                    if self.active_repository.is_some() {
3544                        "No changes to commit"
3545                    } else {
3546                        "No Git repositories"
3547                    },
3548                ))
3549                .children({
3550                    let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
3551                    (worktree_count > 0 && self.active_repository.is_none()).then(|| {
3552                        h_flex().w_full().justify_around().child(
3553                            panel_filled_button("Initialize Repository")
3554                                .tooltip(Tooltip::for_action_title_in(
3555                                    "git init",
3556                                    &git::Init,
3557                                    &self.focus_handle,
3558                                ))
3559                                .on_click(move |_, _, cx| {
3560                                    cx.defer(move |cx| {
3561                                        cx.dispatch_action(&git::Init);
3562                                    })
3563                                }),
3564                        )
3565                    })
3566                })
3567                .text_ui_sm(cx)
3568                .mx_auto()
3569                .text_color(Color::Placeholder.color(cx)),
3570        )
3571    }
3572
3573    fn render_vertical_scrollbar(
3574        &self,
3575        show_horizontal_scrollbar_container: bool,
3576        cx: &mut Context<Self>,
3577    ) -> impl IntoElement {
3578        div()
3579            .id("git-panel-vertical-scroll")
3580            .occlude()
3581            .flex_none()
3582            .h_full()
3583            .cursor_default()
3584            .absolute()
3585            .right_0()
3586            .top_0()
3587            .bottom_0()
3588            .w(px(12.))
3589            .when(show_horizontal_scrollbar_container, |this| {
3590                this.pb_neg_3p5()
3591            })
3592            .on_mouse_move(cx.listener(|_, _, _, cx| {
3593                cx.notify();
3594                cx.stop_propagation()
3595            }))
3596            .on_hover(|_, _, cx| {
3597                cx.stop_propagation();
3598            })
3599            .on_any_mouse_down(|_, _, cx| {
3600                cx.stop_propagation();
3601            })
3602            .on_mouse_up(
3603                MouseButton::Left,
3604                cx.listener(|this, _, window, cx| {
3605                    if !this.vertical_scrollbar.state.is_dragging()
3606                        && !this.focus_handle.contains_focused(window, cx)
3607                    {
3608                        this.vertical_scrollbar.hide(window, cx);
3609                        cx.notify();
3610                    }
3611
3612                    cx.stop_propagation();
3613                }),
3614            )
3615            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
3616                cx.notify();
3617            }))
3618            .children(Scrollbar::vertical(
3619                // percentage as f32..end_offset as f32,
3620                self.vertical_scrollbar.state.clone(),
3621            ))
3622    }
3623
3624    /// Renders the horizontal scrollbar.
3625    ///
3626    /// The right offset is used to determine how far to the right the
3627    /// scrollbar should extend to, useful for ensuring it doesn't collide
3628    /// with the vertical scrollbar when visible.
3629    fn render_horizontal_scrollbar(
3630        &self,
3631        right_offset: Pixels,
3632        cx: &mut Context<Self>,
3633    ) -> impl IntoElement {
3634        div()
3635            .id("git-panel-horizontal-scroll")
3636            .occlude()
3637            .flex_none()
3638            .w_full()
3639            .cursor_default()
3640            .absolute()
3641            .bottom_neg_px()
3642            .left_0()
3643            .right_0()
3644            .pr(right_offset)
3645            .on_mouse_move(cx.listener(|_, _, _, cx| {
3646                cx.notify();
3647                cx.stop_propagation()
3648            }))
3649            .on_hover(|_, _, cx| {
3650                cx.stop_propagation();
3651            })
3652            .on_any_mouse_down(|_, _, cx| {
3653                cx.stop_propagation();
3654            })
3655            .on_mouse_up(
3656                MouseButton::Left,
3657                cx.listener(|this, _, window, cx| {
3658                    if !this.horizontal_scrollbar.state.is_dragging()
3659                        && !this.focus_handle.contains_focused(window, cx)
3660                    {
3661                        this.horizontal_scrollbar.hide(window, cx);
3662                        cx.notify();
3663                    }
3664
3665                    cx.stop_propagation();
3666                }),
3667            )
3668            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
3669                cx.notify();
3670            }))
3671            .children(Scrollbar::horizontal(
3672                // percentage as f32..end_offset as f32,
3673                self.horizontal_scrollbar.state.clone(),
3674            ))
3675    }
3676
3677    fn render_buffer_header_controls(
3678        &self,
3679        entity: &Entity<Self>,
3680        file: &Arc<dyn File>,
3681        _: &Window,
3682        cx: &App,
3683    ) -> Option<AnyElement> {
3684        let repo = self.active_repository.as_ref()?.read(cx);
3685        let project_path = (file.worktree_id(cx), file.path()).into();
3686        let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
3687        let ix = self.entry_by_path(&repo_path, cx)?;
3688        let entry = self.entries.get(ix)?;
3689
3690        let entry_staging = self.entry_staging(entry.status_entry()?);
3691
3692        let checkbox = Checkbox::new("stage-file", entry_staging.as_bool().into())
3693            .disabled(!self.has_write_access(cx))
3694            .fill()
3695            .elevation(ElevationIndex::Surface)
3696            .on_click({
3697                let entry = entry.clone();
3698                let git_panel = entity.downgrade();
3699                move |_, window, cx| {
3700                    git_panel
3701                        .update(cx, |this, cx| {
3702                            this.toggle_staged_for_entry(&entry, window, cx);
3703                            cx.stop_propagation();
3704                        })
3705                        .ok();
3706                }
3707            });
3708        Some(
3709            h_flex()
3710                .id("start-slot")
3711                .text_lg()
3712                .child(checkbox)
3713                .on_mouse_down(MouseButton::Left, |_, _, cx| {
3714                    // prevent the list item active state triggering when toggling checkbox
3715                    cx.stop_propagation();
3716                })
3717                .into_any_element(),
3718        )
3719    }
3720
3721    fn render_entries(
3722        &self,
3723        has_write_access: bool,
3724        _: &Window,
3725        cx: &mut Context<Self>,
3726    ) -> impl IntoElement {
3727        let entry_count = self.entries.len();
3728
3729        let scroll_track_size = px(16.);
3730
3731        let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
3732            // magic number
3733            px(3.)
3734        } else {
3735            px(0.)
3736        };
3737
3738        v_flex()
3739            .flex_1()
3740            .size_full()
3741            .overflow_hidden()
3742            .relative()
3743            // Show a border on the top and bottom of the container when
3744            // the vertical scrollbar container is visible so we don't have a
3745            // floating left border in the panel.
3746            .when(self.vertical_scrollbar.show_track, |this| {
3747                this.border_t_1()
3748                    .border_b_1()
3749                    .border_color(cx.theme().colors().border)
3750            })
3751            .child(
3752                h_flex()
3753                    .flex_1()
3754                    .size_full()
3755                    .relative()
3756                    .overflow_hidden()
3757                    .child(
3758                        uniform_list(
3759                            "entries",
3760                            entry_count,
3761                            cx.processor(move |this, range: Range<usize>, window, cx| {
3762                                let mut items = Vec::with_capacity(range.end - range.start);
3763
3764                                for ix in range {
3765                                    match &this.entries.get(ix) {
3766                                        Some(GitListEntry::Status(entry)) => {
3767                                            items.push(this.render_entry(
3768                                                ix,
3769                                                entry,
3770                                                has_write_access,
3771                                                window,
3772                                                cx,
3773                                            ));
3774                                        }
3775                                        Some(GitListEntry::Header(header)) => {
3776                                            items.push(this.render_list_header(
3777                                                ix,
3778                                                header,
3779                                                has_write_access,
3780                                                window,
3781                                                cx,
3782                                            ));
3783                                        }
3784                                        None => {}
3785                                    }
3786                                }
3787
3788                                items
3789                            }),
3790                        )
3791                        .when(
3792                            !self.horizontal_scrollbar.show_track
3793                                && self.horizontal_scrollbar.show_scrollbar,
3794                            |this| {
3795                                // when not showing the horizontal scrollbar track, make sure we don't
3796                                // obscure the last entry
3797                                this.pb(scroll_track_size)
3798                            },
3799                        )
3800                        .size_full()
3801                        .flex_grow()
3802                        .with_sizing_behavior(ListSizingBehavior::Auto)
3803                        .with_horizontal_sizing_behavior(
3804                            ListHorizontalSizingBehavior::Unconstrained,
3805                        )
3806                        .with_width_from_item(self.max_width_item_index)
3807                        .track_scroll(self.scroll_handle.clone()),
3808                    )
3809                    .on_mouse_down(
3810                        MouseButton::Right,
3811                        cx.listener(move |this, event: &MouseDownEvent, window, cx| {
3812                            this.deploy_panel_context_menu(event.position, window, cx)
3813                        }),
3814                    )
3815                    .when(self.vertical_scrollbar.show_track, |this| {
3816                        this.child(
3817                            v_flex()
3818                                .h_full()
3819                                .flex_none()
3820                                .w(scroll_track_size)
3821                                .bg(cx.theme().colors().panel_background)
3822                                .child(
3823                                    div()
3824                                        .size_full()
3825                                        .flex_1()
3826                                        .border_l_1()
3827                                        .border_color(cx.theme().colors().border),
3828                                ),
3829                        )
3830                    })
3831                    .when(self.vertical_scrollbar.show_scrollbar, |this| {
3832                        this.child(
3833                            self.render_vertical_scrollbar(
3834                                self.horizontal_scrollbar.show_track,
3835                                cx,
3836                            ),
3837                        )
3838                    }),
3839            )
3840            .when(self.horizontal_scrollbar.show_track, |this| {
3841                this.child(
3842                    h_flex()
3843                        .w_full()
3844                        .h(scroll_track_size)
3845                        .flex_none()
3846                        .relative()
3847                        .child(
3848                            div()
3849                                .w_full()
3850                                .flex_1()
3851                                // for some reason the horizontal scrollbar is 1px
3852                                // taller than the vertical scrollbar??
3853                                .h(scroll_track_size - px(1.))
3854                                .bg(cx.theme().colors().panel_background)
3855                                .border_t_1()
3856                                .border_color(cx.theme().colors().border),
3857                        )
3858                        .when(self.vertical_scrollbar.show_track, |this| {
3859                            this.child(
3860                                div()
3861                                    .flex_none()
3862                                    // -1px prevents a missing pixel between the two container borders
3863                                    .w(scroll_track_size - px(1.))
3864                                    .h_full(),
3865                            )
3866                            .child(
3867                                // HACK: Fill the missing 1px 🥲
3868                                div()
3869                                    .absolute()
3870                                    .right(scroll_track_size - px(1.))
3871                                    .bottom(scroll_track_size - px(1.))
3872                                    .size_px()
3873                                    .bg(cx.theme().colors().border),
3874                            )
3875                        }),
3876                )
3877            })
3878            .when(self.horizontal_scrollbar.show_scrollbar, |this| {
3879                this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
3880            })
3881    }
3882
3883    fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
3884        Label::new(label.into()).color(color).single_line()
3885    }
3886
3887    fn list_item_height(&self) -> Rems {
3888        rems(1.75)
3889    }
3890
3891    fn render_list_header(
3892        &self,
3893        ix: usize,
3894        header: &GitHeaderEntry,
3895        _: bool,
3896        _: &Window,
3897        _: &Context<Self>,
3898    ) -> AnyElement {
3899        let id: ElementId = ElementId::Name(format!("header_{}", ix).into());
3900
3901        h_flex()
3902            .id(id)
3903            .h(self.list_item_height())
3904            .w_full()
3905            .items_end()
3906            .px(rems(0.75)) // ~12px
3907            .pb(rems(0.3125)) // ~ 5px
3908            .child(
3909                Label::new(header.title())
3910                    .color(Color::Muted)
3911                    .size(LabelSize::Small)
3912                    .line_height_style(LineHeightStyle::UiLabel)
3913                    .single_line(),
3914            )
3915            .into_any_element()
3916    }
3917
3918    pub fn load_commit_details(
3919        &self,
3920        sha: String,
3921        cx: &mut Context<Self>,
3922    ) -> Task<anyhow::Result<CommitDetails>> {
3923        let Some(repo) = self.active_repository.clone() else {
3924            return Task::ready(Err(anyhow::anyhow!("no active repo")));
3925        };
3926        repo.update(cx, |repo, cx| {
3927            let show = repo.show(sha);
3928            cx.spawn(async move |_, _| show.await?)
3929        })
3930    }
3931
3932    fn deploy_entry_context_menu(
3933        &mut self,
3934        position: Point<Pixels>,
3935        ix: usize,
3936        window: &mut Window,
3937        cx: &mut Context<Self>,
3938    ) {
3939        let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
3940            return;
3941        };
3942        let stage_title = if entry.status.staging().is_fully_staged() {
3943            "Unstage File"
3944        } else {
3945            "Stage File"
3946        };
3947        let restore_title = if entry.status.is_created() {
3948            "Trash File"
3949        } else {
3950            "Restore File"
3951        };
3952        let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
3953            context_menu
3954                .context(self.focus_handle.clone())
3955                .action(stage_title, ToggleStaged.boxed_clone())
3956                .action(restore_title, git::RestoreFile::default().boxed_clone())
3957                .separator()
3958                .action("Open Diff", Confirm.boxed_clone())
3959                .action("Open File", SecondaryConfirm.boxed_clone())
3960        });
3961        self.selected_entry = Some(ix);
3962        self.set_context_menu(context_menu, position, window, cx);
3963    }
3964
3965    fn deploy_panel_context_menu(
3966        &mut self,
3967        position: Point<Pixels>,
3968        window: &mut Window,
3969        cx: &mut Context<Self>,
3970    ) {
3971        let context_menu = git_panel_context_menu(
3972            self.focus_handle.clone(),
3973            GitMenuState {
3974                has_tracked_changes: self.has_tracked_changes(),
3975                has_staged_changes: self.has_staged_changes(),
3976                has_unstaged_changes: self.has_unstaged_changes(),
3977                has_new_changes: self.new_count > 0,
3978            },
3979            window,
3980            cx,
3981        );
3982        self.set_context_menu(context_menu, position, window, cx);
3983    }
3984
3985    fn set_context_menu(
3986        &mut self,
3987        context_menu: Entity<ContextMenu>,
3988        position: Point<Pixels>,
3989        window: &Window,
3990        cx: &mut Context<Self>,
3991    ) {
3992        let subscription = cx.subscribe_in(
3993            &context_menu,
3994            window,
3995            |this, _, _: &DismissEvent, window, cx| {
3996                if this.context_menu.as_ref().is_some_and(|context_menu| {
3997                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
3998                }) {
3999                    cx.focus_self(window);
4000                }
4001                this.context_menu.take();
4002                cx.notify();
4003            },
4004        );
4005        self.context_menu = Some((context_menu, position, subscription));
4006        cx.notify();
4007    }
4008
4009    fn render_entry(
4010        &self,
4011        ix: usize,
4012        entry: &GitStatusEntry,
4013        has_write_access: bool,
4014        window: &Window,
4015        cx: &Context<Self>,
4016    ) -> AnyElement {
4017        let display_name = entry.display_name();
4018
4019        let selected = self.selected_entry == Some(ix);
4020        let marked = self.marked_entries.contains(&ix);
4021        let status_style = GitPanelSettings::get_global(cx).status_style;
4022        let status = entry.status;
4023
4024        let has_conflict = status.is_conflicted();
4025        let is_modified = status.is_modified();
4026        let is_deleted = status.is_deleted();
4027
4028        let label_color = if status_style == StatusStyle::LabelColor {
4029            if has_conflict {
4030                Color::VersionControlConflict
4031            } else if is_modified {
4032                Color::VersionControlModified
4033            } else if is_deleted {
4034                // We don't want a bunch of red labels in the list
4035                Color::Disabled
4036            } else {
4037                Color::VersionControlAdded
4038            }
4039        } else {
4040            Color::Default
4041        };
4042
4043        let path_color = if status.is_deleted() {
4044            Color::Disabled
4045        } else {
4046            Color::Muted
4047        };
4048
4049        let id: ElementId = ElementId::Name(format!("entry_{}_{}", display_name, ix).into());
4050        let checkbox_wrapper_id: ElementId =
4051            ElementId::Name(format!("entry_{}_{}_checkbox_wrapper", display_name, ix).into());
4052        let checkbox_id: ElementId =
4053            ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
4054
4055        let entry_staging = self.entry_staging(entry);
4056        let mut is_staged: ToggleState = self.entry_staging(entry).as_bool().into();
4057        if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() {
4058            is_staged = ToggleState::Selected;
4059        }
4060
4061        let handle = cx.weak_entity();
4062
4063        let selected_bg_alpha = 0.08;
4064        let marked_bg_alpha = 0.12;
4065        let state_opacity_step = 0.04;
4066
4067        let base_bg = match (selected, marked) {
4068            (true, true) => cx
4069                .theme()
4070                .status()
4071                .info
4072                .alpha(selected_bg_alpha + marked_bg_alpha),
4073            (true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
4074            (false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
4075            _ => cx.theme().colors().ghost_element_background,
4076        };
4077
4078        let hover_bg = if selected {
4079            cx.theme()
4080                .status()
4081                .info
4082                .alpha(selected_bg_alpha + state_opacity_step)
4083        } else {
4084            cx.theme().colors().ghost_element_hover
4085        };
4086
4087        let active_bg = if selected {
4088            cx.theme()
4089                .status()
4090                .info
4091                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
4092        } else {
4093            cx.theme().colors().ghost_element_active
4094        };
4095
4096        h_flex()
4097            .id(id)
4098            .h(self.list_item_height())
4099            .w_full()
4100            .items_center()
4101            .border_1()
4102            .when(selected && self.focus_handle.is_focused(window), |el| {
4103                el.border_color(cx.theme().colors().border_focused)
4104            })
4105            .px(rems(0.75)) // ~12px
4106            .overflow_hidden()
4107            .flex_none()
4108            .gap_1p5()
4109            .bg(base_bg)
4110            .hover(|this| this.bg(hover_bg))
4111            .active(|this| this.bg(active_bg))
4112            .on_click({
4113                cx.listener(move |this, event: &ClickEvent, window, cx| {
4114                    this.selected_entry = Some(ix);
4115                    cx.notify();
4116                    if event.modifiers().secondary() {
4117                        this.open_file(&Default::default(), window, cx)
4118                    } else {
4119                        this.open_diff(&Default::default(), window, cx);
4120                        this.focus_handle.focus(window);
4121                    }
4122                })
4123            })
4124            .on_mouse_down(
4125                MouseButton::Right,
4126                move |event: &MouseDownEvent, window, cx| {
4127                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
4128                    if event.button != MouseButton::Right {
4129                        return;
4130                    }
4131
4132                    let Some(this) = handle.upgrade() else {
4133                        return;
4134                    };
4135                    this.update(cx, |this, cx| {
4136                        this.deploy_entry_context_menu(event.position, ix, window, cx);
4137                    });
4138                    cx.stop_propagation();
4139                },
4140            )
4141            .child(
4142                div()
4143                    .id(checkbox_wrapper_id)
4144                    .flex_none()
4145                    .occlude()
4146                    .cursor_pointer()
4147                    .child(
4148                        Checkbox::new(checkbox_id, is_staged)
4149                            .disabled(!has_write_access)
4150                            .fill()
4151                            .elevation(ElevationIndex::Surface)
4152                            .on_click_ext({
4153                                let entry = entry.clone();
4154                                let this = cx.weak_entity();
4155                                move |_, click, window, cx| {
4156                                    this.update(cx, |this, cx| {
4157                                        if !has_write_access {
4158                                            return;
4159                                        }
4160                                        if click.modifiers().shift {
4161                                            this.stage_bulk(ix, cx);
4162                                        } else {
4163                                            this.toggle_staged_for_entry(
4164                                                &GitListEntry::Status(entry.clone()),
4165                                                window,
4166                                                cx,
4167                                            );
4168                                        }
4169                                        cx.stop_propagation();
4170                                    })
4171                                    .ok();
4172                                }
4173                            })
4174                            .tooltip(move |window, cx| {
4175                                let is_staged = entry_staging.is_fully_staged();
4176
4177                                let action = if is_staged { "Unstage" } else { "Stage" };
4178                                let tooltip_name = action.to_string();
4179
4180                                Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
4181                            }),
4182                    ),
4183            )
4184            .child(git_status_icon(status))
4185            .child(
4186                h_flex()
4187                    .items_center()
4188                    .flex_1()
4189                    // .overflow_hidden()
4190                    .when_some(entry.parent_dir(), |this, parent| {
4191                        if !parent.is_empty() {
4192                            this.child(
4193                                self.entry_label(format!("{}/", parent), path_color)
4194                                    .when(status.is_deleted(), |this| this.strikethrough()),
4195                            )
4196                        } else {
4197                            this
4198                        }
4199                    })
4200                    .child(
4201                        self.entry_label(display_name.clone(), label_color)
4202                            .when(status.is_deleted(), |this| this.strikethrough()),
4203                    ),
4204            )
4205            .into_any_element()
4206    }
4207
4208    fn has_write_access(&self, cx: &App) -> bool {
4209        !self.project.read(cx).is_read_only(cx)
4210    }
4211
4212    pub fn amend_pending(&self) -> bool {
4213        self.amend_pending
4214    }
4215
4216    pub fn set_amend_pending(&mut self, value: bool, cx: &mut Context<Self>) {
4217        self.amend_pending = value;
4218        cx.notify();
4219    }
4220
4221    pub async fn load(
4222        workspace: WeakEntity<Workspace>,
4223        mut cx: AsyncWindowContext,
4224    ) -> anyhow::Result<Entity<Self>> {
4225        let serialized_panel = cx
4226            .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
4227            .await
4228            .context("loading git panel")
4229            .log_err()
4230            .flatten()
4231            .and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
4232
4233        workspace.update_in(&mut cx, |workspace, window, cx| {
4234            let panel = GitPanel::new(workspace, window, cx);
4235
4236            if let Some(serialized_panel) = serialized_panel {
4237                panel.update(cx, |panel, cx| {
4238                    panel.width = serialized_panel.width;
4239                    cx.notify();
4240                })
4241            }
4242
4243            panel
4244        })
4245    }
4246
4247    fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) {
4248        let Some(op) = self.bulk_staging.as_ref() else {
4249            return;
4250        };
4251        let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else {
4252            return;
4253        };
4254        if let Some(entry) = self.entries.get(index)
4255            && let Some(entry) = entry.status_entry()
4256        {
4257            self.set_bulk_staging_anchor(entry.repo_path.clone(), cx);
4258        }
4259        if index < anchor_index {
4260            std::mem::swap(&mut index, &mut anchor_index);
4261        }
4262        let entries = self
4263            .entries
4264            .get(anchor_index..=index)
4265            .unwrap_or_default()
4266            .iter()
4267            .filter_map(|entry| entry.status_entry().cloned())
4268            .collect::<Vec<_>>();
4269        self.change_file_stage(true, entries, cx);
4270    }
4271
4272    fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) {
4273        let Some(repo) = self.active_repository.as_ref() else {
4274            return;
4275        };
4276        self.bulk_staging = Some(BulkStaging {
4277            repo_id: repo.read(cx).id,
4278            anchor: path,
4279        });
4280    }
4281}
4282
4283fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
4284    agent_settings::AgentSettings::get_global(cx)
4285        .enabled
4286        .then(|| {
4287            let ConfiguredModel { provider, model } =
4288                LanguageModelRegistry::read_global(cx).commit_message_model()?;
4289
4290            provider.is_authenticated(cx).then(|| model)
4291        })
4292        .flatten()
4293}
4294
4295impl Render for GitPanel {
4296    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4297        let project = self.project.read(cx);
4298        let has_entries = self.entries.len() > 0;
4299        let room = self
4300            .workspace
4301            .upgrade()
4302            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned());
4303
4304        let has_write_access = self.has_write_access(cx);
4305
4306        let has_co_authors = room.map_or(false, |room| {
4307            self.load_local_committer(cx);
4308            let room = room.read(cx);
4309            room.remote_participants()
4310                .values()
4311                .any(|remote_participant| remote_participant.can_write())
4312        });
4313
4314        v_flex()
4315            .id("git_panel")
4316            .key_context(self.dispatch_context(window, cx))
4317            .track_focus(&self.focus_handle)
4318            .when(has_write_access && !project.is_read_only(cx), |this| {
4319                this.on_action(cx.listener(Self::toggle_staged_for_selected))
4320                    .on_action(cx.listener(Self::stage_range))
4321                    .on_action(cx.listener(GitPanel::commit))
4322                    .on_action(cx.listener(GitPanel::amend))
4323                    .on_action(cx.listener(GitPanel::cancel))
4324                    .on_action(cx.listener(Self::stage_all))
4325                    .on_action(cx.listener(Self::unstage_all))
4326                    .on_action(cx.listener(Self::stage_selected))
4327                    .on_action(cx.listener(Self::unstage_selected))
4328                    .on_action(cx.listener(Self::restore_tracked_files))
4329                    .on_action(cx.listener(Self::revert_selected))
4330                    .on_action(cx.listener(Self::clean_all))
4331                    .on_action(cx.listener(Self::generate_commit_message_action))
4332            })
4333            .on_action(cx.listener(Self::select_first))
4334            .on_action(cx.listener(Self::select_next))
4335            .on_action(cx.listener(Self::select_previous))
4336            .on_action(cx.listener(Self::select_last))
4337            .on_action(cx.listener(Self::close_panel))
4338            .on_action(cx.listener(Self::open_diff))
4339            .on_action(cx.listener(Self::open_file))
4340            .on_action(cx.listener(Self::focus_changes_list))
4341            .on_action(cx.listener(Self::focus_editor))
4342            .on_action(cx.listener(Self::expand_commit_editor))
4343            .when(has_write_access && has_co_authors, |git_panel| {
4344                git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
4345            })
4346            .on_hover(cx.listener(move |this, hovered, window, cx| {
4347                if *hovered {
4348                    this.horizontal_scrollbar.show(cx);
4349                    this.vertical_scrollbar.show(cx);
4350                    cx.notify();
4351                } else if !this.focus_handle.contains_focused(window, cx) {
4352                    this.hide_scrollbars(window, cx);
4353                }
4354            }))
4355            .size_full()
4356            .overflow_hidden()
4357            .bg(cx.theme().colors().panel_background)
4358            .child(
4359                v_flex()
4360                    .size_full()
4361                    .children(self.render_panel_header(window, cx))
4362                    .map(|this| {
4363                        if has_entries {
4364                            this.child(self.render_entries(has_write_access, window, cx))
4365                        } else {
4366                            this.child(self.render_empty_state(cx).into_any_element())
4367                        }
4368                    })
4369                    .children(self.render_footer(window, cx))
4370                    .when(self.amend_pending, |this| {
4371                        this.child(self.render_pending_amend(cx))
4372                    })
4373                    .when(!self.amend_pending, |this| {
4374                        this.children(self.render_previous_commit(cx))
4375                    })
4376                    .into_any_element(),
4377            )
4378            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4379                deferred(
4380                    anchored()
4381                        .position(*position)
4382                        .anchor(Corner::TopLeft)
4383                        .child(menu.clone()),
4384                )
4385                .with_priority(1)
4386            }))
4387    }
4388}
4389
4390impl Focusable for GitPanel {
4391    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
4392        if self.entries.is_empty() {
4393            self.commit_editor.focus_handle(cx)
4394        } else {
4395            self.focus_handle.clone()
4396        }
4397    }
4398}
4399
4400impl EventEmitter<Event> for GitPanel {}
4401
4402impl EventEmitter<PanelEvent> for GitPanel {}
4403
4404pub(crate) struct GitPanelAddon {
4405    pub(crate) workspace: WeakEntity<Workspace>,
4406}
4407
4408impl editor::Addon for GitPanelAddon {
4409    fn to_any(&self) -> &dyn std::any::Any {
4410        self
4411    }
4412
4413    fn render_buffer_header_controls(
4414        &self,
4415        excerpt_info: &ExcerptInfo,
4416        window: &Window,
4417        cx: &App,
4418    ) -> Option<AnyElement> {
4419        let file = excerpt_info.buffer.file()?;
4420        let git_panel = self.workspace.upgrade()?.read(cx).panel::<GitPanel>(cx)?;
4421
4422        git_panel
4423            .read(cx)
4424            .render_buffer_header_controls(&git_panel, &file, window, cx)
4425    }
4426}
4427
4428impl Panel for GitPanel {
4429    fn persistent_name() -> &'static str {
4430        "GitPanel"
4431    }
4432
4433    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4434        GitPanelSettings::get_global(cx).dock
4435    }
4436
4437    fn position_is_valid(&self, position: DockPosition) -> bool {
4438        matches!(position, DockPosition::Left | DockPosition::Right)
4439    }
4440
4441    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4442        settings::update_settings_file::<GitPanelSettings>(
4443            self.fs.clone(),
4444            cx,
4445            move |settings, _| settings.dock = Some(position),
4446        );
4447    }
4448
4449    fn size(&self, _: &Window, cx: &App) -> Pixels {
4450        self.width
4451            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
4452    }
4453
4454    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4455        self.width = size;
4456        self.serialize(cx);
4457        cx.notify();
4458    }
4459
4460    fn icon(&self, _: &Window, cx: &App) -> Option<ui::IconName> {
4461        Some(ui::IconName::GitBranchSmall).filter(|_| GitPanelSettings::get_global(cx).button)
4462    }
4463
4464    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4465        Some("Git Panel")
4466    }
4467
4468    fn toggle_action(&self) -> Box<dyn Action> {
4469        Box::new(ToggleFocus)
4470    }
4471
4472    fn activation_priority(&self) -> u32 {
4473        2
4474    }
4475}
4476
4477impl PanelHeader for GitPanel {}
4478
4479struct GitPanelMessageTooltip {
4480    commit_tooltip: Option<Entity<CommitTooltip>>,
4481}
4482
4483impl GitPanelMessageTooltip {
4484    fn new(
4485        git_panel: Entity<GitPanel>,
4486        sha: SharedString,
4487        repository: Entity<Repository>,
4488        window: &mut Window,
4489        cx: &mut App,
4490    ) -> Entity<Self> {
4491        cx.new(|cx| {
4492            cx.spawn_in(window, async move |this, cx| {
4493                let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
4494                    (
4495                        git_panel.load_commit_details(sha.to_string(), cx),
4496                        git_panel.workspace.clone(),
4497                    )
4498                })?;
4499                let details = details.await?;
4500
4501                let commit_details = crate::commit_tooltip::CommitDetails {
4502                    sha: details.sha.clone(),
4503                    author_name: details.author_name.clone(),
4504                    author_email: details.author_email.clone(),
4505                    commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
4506                    message: Some(ParsedCommitMessage {
4507                        message: details.message.clone(),
4508                        ..Default::default()
4509                    }),
4510                };
4511
4512                this.update(cx, |this: &mut GitPanelMessageTooltip, cx| {
4513                    this.commit_tooltip = Some(cx.new(move |cx| {
4514                        CommitTooltip::new(commit_details, repository, workspace, cx)
4515                    }));
4516                    cx.notify();
4517                })
4518            })
4519            .detach();
4520
4521            Self {
4522                commit_tooltip: None,
4523            }
4524        })
4525    }
4526}
4527
4528impl Render for GitPanelMessageTooltip {
4529    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
4530        if let Some(commit_tooltip) = &self.commit_tooltip {
4531            commit_tooltip.clone().into_any_element()
4532        } else {
4533            gpui::Empty.into_any_element()
4534        }
4535    }
4536}
4537
4538#[derive(IntoElement, RegisterComponent)]
4539pub struct PanelRepoFooter {
4540    active_repository: SharedString,
4541    branch: Option<Branch>,
4542    head_commit: Option<CommitDetails>,
4543
4544    // Getting a GitPanel in previews will be difficult.
4545    //
4546    // For now just take an option here, and we won't bind handlers to buttons in previews.
4547    git_panel: Option<Entity<GitPanel>>,
4548}
4549
4550impl PanelRepoFooter {
4551    pub fn new(
4552        active_repository: SharedString,
4553        branch: Option<Branch>,
4554        head_commit: Option<CommitDetails>,
4555        git_panel: Option<Entity<GitPanel>>,
4556    ) -> Self {
4557        Self {
4558            active_repository,
4559            branch,
4560            head_commit,
4561            git_panel,
4562        }
4563    }
4564
4565    pub fn new_preview(active_repository: SharedString, branch: Option<Branch>) -> Self {
4566        Self {
4567            active_repository,
4568            branch,
4569            head_commit: None,
4570            git_panel: None,
4571        }
4572    }
4573}
4574
4575impl RenderOnce for PanelRepoFooter {
4576    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
4577        let project = self
4578            .git_panel
4579            .as_ref()
4580            .map(|panel| panel.read(cx).project.clone());
4581
4582        let repo = self
4583            .git_panel
4584            .as_ref()
4585            .and_then(|panel| panel.read(cx).active_repository.clone());
4586
4587        let single_repo = project
4588            .as_ref()
4589            .map(|project| project.read(cx).git_store().read(cx).repositories().len() == 1)
4590            .unwrap_or(true);
4591
4592        const MAX_BRANCH_LEN: usize = 16;
4593        const MAX_REPO_LEN: usize = 16;
4594        const LABEL_CHARACTER_BUDGET: usize = MAX_BRANCH_LEN + MAX_REPO_LEN;
4595        const MAX_SHORT_SHA_LEN: usize = 8;
4596
4597        let branch_name = self
4598            .branch
4599            .as_ref()
4600            .map(|branch| branch.name().to_owned())
4601            .or_else(|| {
4602                self.head_commit.as_ref().map(|commit| {
4603                    commit
4604                        .sha
4605                        .chars()
4606                        .take(MAX_SHORT_SHA_LEN)
4607                        .collect::<String>()
4608                })
4609            })
4610            .unwrap_or_else(|| " (no branch)".to_owned());
4611        let show_separator = self.branch.is_some() || self.head_commit.is_some();
4612
4613        let active_repo_name = self.active_repository.clone();
4614
4615        let branch_actual_len = branch_name.len();
4616        let repo_actual_len = active_repo_name.len();
4617
4618        // ideally, show the whole branch and repo names but
4619        // when we can't, use a budget to allocate space between the two
4620        let (repo_display_len, branch_display_len) = if branch_actual_len + repo_actual_len
4621            <= LABEL_CHARACTER_BUDGET
4622        {
4623            (repo_actual_len, branch_actual_len)
4624        } else {
4625            if branch_actual_len <= MAX_BRANCH_LEN {
4626                let repo_space = (LABEL_CHARACTER_BUDGET - branch_actual_len).min(MAX_REPO_LEN);
4627                (repo_space, branch_actual_len)
4628            } else if repo_actual_len <= MAX_REPO_LEN {
4629                let branch_space = (LABEL_CHARACTER_BUDGET - repo_actual_len).min(MAX_BRANCH_LEN);
4630                (repo_actual_len, branch_space)
4631            } else {
4632                (MAX_REPO_LEN, MAX_BRANCH_LEN)
4633            }
4634        };
4635
4636        let truncated_repo_name = if repo_actual_len <= repo_display_len {
4637            active_repo_name.to_string()
4638        } else {
4639            util::truncate_and_trailoff(active_repo_name.trim_ascii(), repo_display_len)
4640        };
4641
4642        let truncated_branch_name = if branch_actual_len <= branch_display_len {
4643            branch_name.to_string()
4644        } else {
4645            util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
4646        };
4647
4648        let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name)
4649            .style(ButtonStyle::Transparent)
4650            .size(ButtonSize::None)
4651            .label_size(LabelSize::Small)
4652            .color(Color::Muted);
4653
4654        let repo_selector = PopoverMenu::new("repository-switcher")
4655            .menu({
4656                let project = project.clone();
4657                move |window, cx| {
4658                    let project = project.clone()?;
4659                    Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx)))
4660                }
4661            })
4662            .trigger_with_tooltip(
4663                repo_selector_trigger.disabled(single_repo).truncate(true),
4664                Tooltip::text("Switch Active Repository"),
4665            )
4666            .anchor(Corner::BottomLeft)
4667            .into_any_element();
4668
4669        let branch_selector_button = Button::new("branch-selector", truncated_branch_name)
4670            .style(ButtonStyle::Transparent)
4671            .size(ButtonSize::None)
4672            .label_size(LabelSize::Small)
4673            .truncate(true)
4674            .tooltip(Tooltip::for_action_title(
4675                "Switch Branch",
4676                &zed_actions::git::Switch,
4677            ))
4678            .on_click(|_, window, cx| {
4679                window.dispatch_action(zed_actions::git::Switch.boxed_clone(), cx);
4680            });
4681
4682        let branch_selector = PopoverMenu::new("popover-button")
4683            .menu(move |window, cx| Some(branch_picker::popover(repo.clone(), window, cx)))
4684            .trigger_with_tooltip(
4685                branch_selector_button,
4686                Tooltip::for_action_title("Switch Branch", &zed_actions::git::Switch),
4687            )
4688            .anchor(Corner::BottomLeft)
4689            .offset(gpui::Point {
4690                x: px(0.0),
4691                y: px(-2.0),
4692            });
4693
4694        h_flex()
4695            .w_full()
4696            .px_2()
4697            .h(px(36.))
4698            .items_center()
4699            .justify_between()
4700            .gap_1()
4701            .child(
4702                h_flex()
4703                    .flex_1()
4704                    .overflow_hidden()
4705                    .items_center()
4706                    .child(
4707                        div().child(
4708                            Icon::new(IconName::GitBranchSmall)
4709                                .size(IconSize::Small)
4710                                .color(if single_repo {
4711                                    Color::Disabled
4712                                } else {
4713                                    Color::Muted
4714                                }),
4715                        ),
4716                    )
4717                    .child(repo_selector)
4718                    .when(show_separator, |this| {
4719                        this.child(
4720                            div()
4721                                .text_color(cx.theme().colors().text_muted)
4722                                .text_sm()
4723                                .child("/"),
4724                        )
4725                    })
4726                    .child(branch_selector),
4727            )
4728            .children(if let Some(git_panel) = self.git_panel {
4729                git_panel.update(cx, |git_panel, cx| git_panel.render_remote_button(cx))
4730            } else {
4731                None
4732            })
4733    }
4734}
4735
4736impl Component for PanelRepoFooter {
4737    fn scope() -> ComponentScope {
4738        ComponentScope::VersionControl
4739    }
4740
4741    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
4742        let unknown_upstream = None;
4743        let no_remote_upstream = Some(UpstreamTracking::Gone);
4744        let ahead_of_upstream = Some(
4745            UpstreamTrackingStatus {
4746                ahead: 2,
4747                behind: 0,
4748            }
4749            .into(),
4750        );
4751        let behind_upstream = Some(
4752            UpstreamTrackingStatus {
4753                ahead: 0,
4754                behind: 2,
4755            }
4756            .into(),
4757        );
4758        let ahead_and_behind_upstream = Some(
4759            UpstreamTrackingStatus {
4760                ahead: 3,
4761                behind: 1,
4762            }
4763            .into(),
4764        );
4765
4766        let not_ahead_or_behind_upstream = Some(
4767            UpstreamTrackingStatus {
4768                ahead: 0,
4769                behind: 0,
4770            }
4771            .into(),
4772        );
4773
4774        fn branch(upstream: Option<UpstreamTracking>) -> Branch {
4775            Branch {
4776                is_head: true,
4777                ref_name: "some-branch".into(),
4778                upstream: upstream.map(|tracking| Upstream {
4779                    ref_name: "origin/some-branch".into(),
4780                    tracking,
4781                }),
4782                most_recent_commit: Some(CommitSummary {
4783                    sha: "abc123".into(),
4784                    subject: "Modify stuff".into(),
4785                    commit_timestamp: 1710932954,
4786                    has_parent: true,
4787                }),
4788            }
4789        }
4790
4791        fn custom(branch_name: &str, upstream: Option<UpstreamTracking>) -> Branch {
4792            Branch {
4793                is_head: true,
4794                ref_name: branch_name.to_string().into(),
4795                upstream: upstream.map(|tracking| Upstream {
4796                    ref_name: format!("zed/{}", branch_name).into(),
4797                    tracking,
4798                }),
4799                most_recent_commit: Some(CommitSummary {
4800                    sha: "abc123".into(),
4801                    subject: "Modify stuff".into(),
4802                    commit_timestamp: 1710932954,
4803                    has_parent: true,
4804                }),
4805            }
4806        }
4807
4808        fn active_repository(id: usize) -> SharedString {
4809            format!("repo-{}", id).into()
4810        }
4811
4812        let example_width = px(340.);
4813        Some(
4814            v_flex()
4815                .gap_6()
4816                .w_full()
4817                .flex_none()
4818                .children(vec![
4819                    example_group_with_title(
4820                        "Action Button States",
4821                        vec![
4822                            single_example(
4823                                "No Branch",
4824                                div()
4825                                    .w(example_width)
4826                                    .overflow_hidden()
4827                                    .child(PanelRepoFooter::new_preview(
4828                                        active_repository(1).clone(),
4829                                        None,
4830                                    ))
4831                                    .into_any_element(),
4832                            ),
4833                            single_example(
4834                                "Remote status unknown",
4835                                div()
4836                                    .w(example_width)
4837                                    .overflow_hidden()
4838                                    .child(PanelRepoFooter::new_preview(
4839                                        active_repository(2).clone(),
4840                                        Some(branch(unknown_upstream)),
4841                                    ))
4842                                    .into_any_element(),
4843                            ),
4844                            single_example(
4845                                "No Remote Upstream",
4846                                div()
4847                                    .w(example_width)
4848                                    .overflow_hidden()
4849                                    .child(PanelRepoFooter::new_preview(
4850                                        active_repository(3).clone(),
4851                                        Some(branch(no_remote_upstream)),
4852                                    ))
4853                                    .into_any_element(),
4854                            ),
4855                            single_example(
4856                                "Not Ahead or Behind",
4857                                div()
4858                                    .w(example_width)
4859                                    .overflow_hidden()
4860                                    .child(PanelRepoFooter::new_preview(
4861                                        active_repository(4).clone(),
4862                                        Some(branch(not_ahead_or_behind_upstream)),
4863                                    ))
4864                                    .into_any_element(),
4865                            ),
4866                            single_example(
4867                                "Behind remote",
4868                                div()
4869                                    .w(example_width)
4870                                    .overflow_hidden()
4871                                    .child(PanelRepoFooter::new_preview(
4872                                        active_repository(5).clone(),
4873                                        Some(branch(behind_upstream)),
4874                                    ))
4875                                    .into_any_element(),
4876                            ),
4877                            single_example(
4878                                "Ahead of remote",
4879                                div()
4880                                    .w(example_width)
4881                                    .overflow_hidden()
4882                                    .child(PanelRepoFooter::new_preview(
4883                                        active_repository(6).clone(),
4884                                        Some(branch(ahead_of_upstream)),
4885                                    ))
4886                                    .into_any_element(),
4887                            ),
4888                            single_example(
4889                                "Ahead and behind remote",
4890                                div()
4891                                    .w(example_width)
4892                                    .overflow_hidden()
4893                                    .child(PanelRepoFooter::new_preview(
4894                                        active_repository(7).clone(),
4895                                        Some(branch(ahead_and_behind_upstream)),
4896                                    ))
4897                                    .into_any_element(),
4898                            ),
4899                        ],
4900                    )
4901                    .grow()
4902                    .vertical(),
4903                ])
4904                .children(vec![
4905                    example_group_with_title(
4906                        "Labels",
4907                        vec![
4908                            single_example(
4909                                "Short Branch & Repo",
4910                                div()
4911                                    .w(example_width)
4912                                    .overflow_hidden()
4913                                    .child(PanelRepoFooter::new_preview(
4914                                        SharedString::from("zed"),
4915                                        Some(custom("main", behind_upstream)),
4916                                    ))
4917                                    .into_any_element(),
4918                            ),
4919                            single_example(
4920                                "Long Branch",
4921                                div()
4922                                    .w(example_width)
4923                                    .overflow_hidden()
4924                                    .child(PanelRepoFooter::new_preview(
4925                                        SharedString::from("zed"),
4926                                        Some(custom(
4927                                            "redesign-and-update-git-ui-list-entry-style",
4928                                            behind_upstream,
4929                                        )),
4930                                    ))
4931                                    .into_any_element(),
4932                            ),
4933                            single_example(
4934                                "Long Repo",
4935                                div()
4936                                    .w(example_width)
4937                                    .overflow_hidden()
4938                                    .child(PanelRepoFooter::new_preview(
4939                                        SharedString::from("zed-industries-community-examples"),
4940                                        Some(custom("gpui", ahead_of_upstream)),
4941                                    ))
4942                                    .into_any_element(),
4943                            ),
4944                            single_example(
4945                                "Long Repo & Branch",
4946                                div()
4947                                    .w(example_width)
4948                                    .overflow_hidden()
4949                                    .child(PanelRepoFooter::new_preview(
4950                                        SharedString::from("zed-industries-community-examples"),
4951                                        Some(custom(
4952                                            "redesign-and-update-git-ui-list-entry-style",
4953                                            behind_upstream,
4954                                        )),
4955                                    ))
4956                                    .into_any_element(),
4957                            ),
4958                            single_example(
4959                                "Uppercase Repo",
4960                                div()
4961                                    .w(example_width)
4962                                    .overflow_hidden()
4963                                    .child(PanelRepoFooter::new_preview(
4964                                        SharedString::from("LICENSES"),
4965                                        Some(custom("main", ahead_of_upstream)),
4966                                    ))
4967                                    .into_any_element(),
4968                            ),
4969                            single_example(
4970                                "Uppercase Branch",
4971                                div()
4972                                    .w(example_width)
4973                                    .overflow_hidden()
4974                                    .child(PanelRepoFooter::new_preview(
4975                                        SharedString::from("zed"),
4976                                        Some(custom("update-README", behind_upstream)),
4977                                    ))
4978                                    .into_any_element(),
4979                            ),
4980                        ],
4981                    )
4982                    .grow()
4983                    .vertical(),
4984                ])
4985                .into_any_element(),
4986        )
4987    }
4988}
4989
4990#[cfg(test)]
4991mod tests {
4992    use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
4993    use gpui::{TestAppContext, VisualTestContext};
4994    use project::{FakeFs, WorktreeSettings};
4995    use serde_json::json;
4996    use settings::SettingsStore;
4997    use theme::LoadThemes;
4998    use util::path;
4999
5000    use super::*;
5001
5002    fn init_test(cx: &mut gpui::TestAppContext) {
5003        zlog::init_test();
5004
5005        cx.update(|cx| {
5006            let settings_store = SettingsStore::test(cx);
5007            cx.set_global(settings_store);
5008            AgentSettings::register(cx);
5009            WorktreeSettings::register(cx);
5010            workspace::init_settings(cx);
5011            theme::init(LoadThemes::JustBase, cx);
5012            language::init(cx);
5013            editor::init(cx);
5014            Project::init_settings(cx);
5015            crate::init(cx);
5016        });
5017    }
5018
5019    #[gpui::test]
5020    async fn test_entry_worktree_paths(cx: &mut TestAppContext) {
5021        init_test(cx);
5022        let fs = FakeFs::new(cx.background_executor.clone());
5023        fs.insert_tree(
5024            "/root",
5025            json!({
5026                "zed": {
5027                    ".git": {},
5028                    "crates": {
5029                        "gpui": {
5030                            "gpui.rs": "fn main() {}"
5031                        },
5032                        "util": {
5033                            "util.rs": "fn do_it() {}"
5034                        }
5035                    }
5036                },
5037            }),
5038        )
5039        .await;
5040
5041        fs.set_status_for_repo(
5042            Path::new(path!("/root/zed/.git")),
5043            &[
5044                (
5045                    Path::new("crates/gpui/gpui.rs"),
5046                    StatusCode::Modified.worktree(),
5047                ),
5048                (
5049                    Path::new("crates/util/util.rs"),
5050                    StatusCode::Modified.worktree(),
5051                ),
5052            ],
5053        );
5054
5055        let project =
5056            Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
5057        let workspace =
5058            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5059        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5060
5061        cx.read(|cx| {
5062            project
5063                .read(cx)
5064                .worktrees(cx)
5065                .nth(0)
5066                .unwrap()
5067                .read(cx)
5068                .as_local()
5069                .unwrap()
5070                .scan_complete()
5071        })
5072        .await;
5073
5074        cx.executor().run_until_parked();
5075
5076        let panel = workspace.update(cx, GitPanel::new).unwrap();
5077
5078        let handle = cx.update_window_entity(&panel, |panel, _, _| {
5079            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
5080        });
5081        cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
5082        handle.await;
5083
5084        let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
5085        pretty_assertions::assert_eq!(
5086            entries,
5087            [
5088                GitListEntry::Header(GitHeaderEntry {
5089                    header: Section::Tracked
5090                }),
5091                GitListEntry::Status(GitStatusEntry {
5092                    abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
5093                    repo_path: "crates/gpui/gpui.rs".into(),
5094                    status: StatusCode::Modified.worktree(),
5095                    staging: StageStatus::Unstaged,
5096                }),
5097                GitListEntry::Status(GitStatusEntry {
5098                    abs_path: path!("/root/zed/crates/util/util.rs").into(),
5099                    repo_path: "crates/util/util.rs".into(),
5100                    status: StatusCode::Modified.worktree(),
5101                    staging: StageStatus::Unstaged,
5102                },),
5103            ],
5104        );
5105
5106        let handle = cx.update_window_entity(&panel, |panel, _, _| {
5107            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
5108        });
5109        cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
5110        handle.await;
5111        let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
5112        pretty_assertions::assert_eq!(
5113            entries,
5114            [
5115                GitListEntry::Header(GitHeaderEntry {
5116                    header: Section::Tracked
5117                }),
5118                GitListEntry::Status(GitStatusEntry {
5119                    abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
5120                    repo_path: "crates/gpui/gpui.rs".into(),
5121                    status: StatusCode::Modified.worktree(),
5122                    staging: StageStatus::Unstaged,
5123                }),
5124                GitListEntry::Status(GitStatusEntry {
5125                    abs_path: path!("/root/zed/crates/util/util.rs").into(),
5126                    repo_path: "crates/util/util.rs".into(),
5127                    status: StatusCode::Modified.worktree(),
5128                    staging: StageStatus::Unstaged,
5129                },),
5130            ],
5131        );
5132    }
5133
5134    #[gpui::test]
5135    async fn test_bulk_staging(cx: &mut TestAppContext) {
5136        use GitListEntry::*;
5137
5138        init_test(cx);
5139        let fs = FakeFs::new(cx.background_executor.clone());
5140        fs.insert_tree(
5141            "/root",
5142            json!({
5143                "project": {
5144                    ".git": {},
5145                    "src": {
5146                        "main.rs": "fn main() {}",
5147                        "lib.rs": "pub fn hello() {}",
5148                        "utils.rs": "pub fn util() {}"
5149                    },
5150                    "tests": {
5151                        "test.rs": "fn test() {}"
5152                    },
5153                    "new_file.txt": "new content",
5154                    "another_new.rs": "// new file",
5155                    "conflict.txt": "conflicted content"
5156                }
5157            }),
5158        )
5159        .await;
5160
5161        fs.set_status_for_repo(
5162            Path::new(path!("/root/project/.git")),
5163            &[
5164                (Path::new("src/main.rs"), StatusCode::Modified.worktree()),
5165                (Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
5166                (Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
5167                (Path::new("new_file.txt"), FileStatus::Untracked),
5168                (Path::new("another_new.rs"), FileStatus::Untracked),
5169                (Path::new("src/utils.rs"), FileStatus::Untracked),
5170                (
5171                    Path::new("conflict.txt"),
5172                    UnmergedStatus {
5173                        first_head: UnmergedStatusCode::Updated,
5174                        second_head: UnmergedStatusCode::Updated,
5175                    }
5176                    .into(),
5177                ),
5178            ],
5179        );
5180
5181        let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
5182        let workspace =
5183            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5184        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5185
5186        cx.read(|cx| {
5187            project
5188                .read(cx)
5189                .worktrees(cx)
5190                .nth(0)
5191                .unwrap()
5192                .read(cx)
5193                .as_local()
5194                .unwrap()
5195                .scan_complete()
5196        })
5197        .await;
5198
5199        cx.executor().run_until_parked();
5200
5201        let panel = workspace.update(cx, GitPanel::new).unwrap();
5202
5203        let handle = cx.update_window_entity(&panel, |panel, _, _| {
5204            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
5205        });
5206        cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
5207        handle.await;
5208
5209        let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
5210        #[rustfmt::skip]
5211        pretty_assertions::assert_matches!(
5212            entries.as_slice(),
5213            &[
5214                Header(GitHeaderEntry { header: Section::Conflict }),
5215                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5216                Header(GitHeaderEntry { header: Section::Tracked }),
5217                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5218                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5219                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5220                Header(GitHeaderEntry { header: Section::New }),
5221                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5222                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5223                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5224            ],
5225        );
5226
5227        let second_status_entry = entries[3].clone();
5228        panel.update_in(cx, |panel, window, cx| {
5229            panel.toggle_staged_for_entry(&second_status_entry, window, cx);
5230        });
5231
5232        panel.update_in(cx, |panel, window, cx| {
5233            panel.selected_entry = Some(7);
5234            panel.stage_range(&git::StageRange, window, cx);
5235        });
5236
5237        cx.read(|cx| {
5238            project
5239                .read(cx)
5240                .worktrees(cx)
5241                .nth(0)
5242                .unwrap()
5243                .read(cx)
5244                .as_local()
5245                .unwrap()
5246                .scan_complete()
5247        })
5248        .await;
5249
5250        cx.executor().run_until_parked();
5251
5252        let handle = cx.update_window_entity(&panel, |panel, _, _| {
5253            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
5254        });
5255        cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
5256        handle.await;
5257
5258        let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
5259        #[rustfmt::skip]
5260        pretty_assertions::assert_matches!(
5261            entries.as_slice(),
5262            &[
5263                Header(GitHeaderEntry { header: Section::Conflict }),
5264                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5265                Header(GitHeaderEntry { header: Section::Tracked }),
5266                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5267                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5268                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5269                Header(GitHeaderEntry { header: Section::New }),
5270                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5271                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5272                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5273            ],
5274        );
5275
5276        let third_status_entry = entries[4].clone();
5277        panel.update_in(cx, |panel, window, cx| {
5278            panel.toggle_staged_for_entry(&third_status_entry, window, cx);
5279        });
5280
5281        panel.update_in(cx, |panel, window, cx| {
5282            panel.selected_entry = Some(9);
5283            panel.stage_range(&git::StageRange, window, cx);
5284        });
5285
5286        cx.read(|cx| {
5287            project
5288                .read(cx)
5289                .worktrees(cx)
5290                .nth(0)
5291                .unwrap()
5292                .read(cx)
5293                .as_local()
5294                .unwrap()
5295                .scan_complete()
5296        })
5297        .await;
5298
5299        cx.executor().run_until_parked();
5300
5301        let handle = cx.update_window_entity(&panel, |panel, _, _| {
5302            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
5303        });
5304        cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
5305        handle.await;
5306
5307        let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
5308        #[rustfmt::skip]
5309        pretty_assertions::assert_matches!(
5310            entries.as_slice(),
5311            &[
5312                Header(GitHeaderEntry { header: Section::Conflict }),
5313                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5314                Header(GitHeaderEntry { header: Section::Tracked }),
5315                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5316                Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
5317                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5318                Header(GitHeaderEntry { header: Section::New }),
5319                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5320                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5321                Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
5322            ],
5323        );
5324    }
5325}