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