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