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