git_panel.rs

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