git_panel.rs

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