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