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