12652 lines · 465.4 KB · Rust
RawDownload
use crate::askpass_modal::AskPassModal;use crate::commit_context_menu::{    CommitContextMenuData, CommitContextMenuSource, commit_context_menu,};use crate::commit_modal::CommitModal;use crate::commit_tooltip::{CommitAvatar, CommitTooltip};use crate::commit_view::CommitView;use crate::git_panel_settings::GitPanelScrollbarAccessor;use crate::project_diff::{DeployBranchDiff, Diff, ProjectDiff};use crate::remote_output::{self, RemoteAction, SuccessMessage};use crate::solo_diff_view::SoloDiffView;use crate::staged_diff::StagedDiff;use crate::unstaged_diff::UnstagedDiff;use crate::{branch_picker, picker_prompt, render_remote_button};use crate::{    git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,};use agent_settings::{AgentSettings, UserAgentsMd};use anyhow::Context as _;use askpass::AskPassDelegate;use client::zed_urls;use collections::{BTreeMap, HashMap, HashSet};use db::kvp::KeyValueStore;use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior};use editor::{EditorStyle, RewrapOptions};use file_icons::FileIcons;use futures::StreamExt as _;use futures::channel::oneshot::Canceled;use git::Oid;use git::commit::ParsedCommitMessage;use git::repository::{    Branch, CommitData, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions,    GitCommitTemplate, GitCommitter, InitialGraphCommitData, LogOrder, LogSource, PushOptions,    Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,    get_git_committer,};use git::stash::GitStash;use git::status::{DiffStat, StageStatus};use git::{    Amend, Commit, Signoff, SkipHooks, ToggleStaged, repository::RepoPath, status::FileStatus,};use git::{    ExpandCommitEditor, GitHostingProviderRegistry, GitRemote, RestoreTrackedFiles, StageAll,    StashAll, StashApply, StashPop, ToggleFillCommitEditor, TrashUntrackedFiles, UnstageAll,    ViewFile, parse_git_remote_url,};use gpui::{    AbsoluteLength, Action, Anchor, AnyElement, AsyncApp, AsyncWindowContext, ClickEvent,    DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton,    MouseDownEvent, Pixels, Point, PromptLevel, ScrollStrategy, Subscription, Task, TaskExt,    TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,};use itertools::Itertools;use language::{Buffer, BufferEvent, File};use language_model::{    CompletionIntent, ConfiguredModel, Event as LanguageModelEvent, LanguageModelRegistry,    LanguageModelRequest, LanguageModelRequestMessage, Role,};use menu;use multi_buffer::ExcerptBoundaryInfo;use notifications::status_toast::StatusToast;use panel::PanelHeader;use project::git_store::GitAccess;use project::{    Fs, Project, ProjectPath,    git_store::{        CommitDataState, GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op,    },    project_settings::{GitPathStyle, ProjectSettings},};use prompt_store::RULES_FILE_NAMES;use proto::RpcError;use serde::{Deserialize, Serialize};use settings::{    GitPanelClickBehavior, GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore, StatusStyle,    update_settings_file,};use smallvec::SmallVec;use std::cell::Cell;use std::future::Future;use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::{sync::Arc, time::Duration, usize};use strum::{IntoEnumIterator, VariantNames};use theme_settings::ThemeSettings;use time::OffsetDateTime;use ui::{    ButtonLike, Checkbox, Chip, ContextMenu, ContextMenuEntry, Divider, DocumentationSide,    ElevationIndex, IndentGuideColors, KeyBinding, PopoverMenu, PopoverMenuHandle,    ProjectEmptyState, ScrollAxes, Scrollbars, SplitButton, Tab, TintColor, Tooltip, WithScrollbar,    prelude::*,};use util::paths::PathStyle;use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath};use workspace::SERIALIZATION_THROTTLE_TIME;use workspace::{    Item, Workspace,    dock::{DockPosition, Panel, PanelEvent},    notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},};use zed_actions::{    DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, git_panel::ToggleFocus,}; const GIT_PANEL_KEY: &str = "GitPanel";const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panelconst TREE_INDENT: f32 = 16.0;const MAX_HISTORY_TAG_CHIPS: usize = 3;// Horizontal offset that aligns the tree indent guides with the row icon column.const INDENT_GUIDE_LEFT_OFFSET: gpui::Pixels = gpui::px(19.); actions!(    git_panel,    [        /// Closes the git panel.        Close,        /// Toggles the git panel.        Toggle,        /// Opens the git panel menu.        OpenMenu,        /// Focuses on the commit message editor.        FocusEditor,        /// Focuses on the changes list.        FocusChanges,        /// Select next git panel menu item, and show it in the diff view        NextEntry,        /// Select previous git panel menu item, and show it in the diff view        PreviousEntry,        /// Select first git panel menu item, and show it in the diff view        FirstEntry,        /// Select last git panel menu item, and show it in the diff view        LastEntry,        /// Toggles automatic co-author suggestions.        ToggleFillCoAuthors,        /// Sorts entries by path.        SetSortByPath,        /// Sorts entries by name.        SetSortByName,        /// Disables grouping entries by status.        SetGroupByNone,        /// Groups entries by status.        SetGroupByStatus,        /// Groups entries by staging state.        SetGroupByStaging,        /// Toggles showing entries in tree vs flat view.        ToggleTreeView,        /// Expands the selected entry to show its children.        ExpandSelectedEntry,        /// Collapses the selected entry to hide its children.        CollapseSelectedEntry,        /// View unstaged changes        ViewUnstagedChanges,        /// View staged changes        ViewStagedChanges,        /// Activates the Changes tab.        ActivateChangesTab,        /// Activates the History tab.        ActivateHistoryTab,    ]); actions!(    dev,    [        /// Shows the current git job queue debug state for the active repository.        ShowGitJobQueue,    ]); fn prompt<T>(    msg: &str,    detail: Option<&str>,    window: &mut Window,    cx: &mut App,) -> Task<anyhow::Result<T>>where    T: IntoEnumIterator + VariantNames + 'static,{    let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx);    cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))} #[derive(strum::EnumIter, strum::VariantNames)]#[strum(serialize_all = "title_case")]enum TrashCancel {    Trash,    Cancel,} #[derive(Clone, Copy)]struct GitPanelViewOptionsMenuState {    sort_by: GitPanelSortBy,    group_by: GitPanelGroupBy,    tree_view: bool,} fn git_panel_context_menu(    has_tracked_changes: bool,    has_staged_changes: bool,    has_unstaged_changes: bool,    has_new_changes: bool,    has_stash_items: bool,    focus_handle: FocusHandle,    window: &mut Window,    cx: &mut App,) -> Entity<ContextMenu> {    ContextMenu::build(window, cx, |context_menu, _, _| {        context_menu            .context(focus_handle.clone())            .action_disabled_when(!has_unstaged_changes, "Stage All", StageAll.boxed_clone())            .action_disabled_when(!has_staged_changes, "Unstage All", UnstageAll.boxed_clone())            .action_disabled_when(                !has_tracked_changes,                "Restore All Changes",                RestoreTrackedFiles.boxed_clone(),            )            .separator()            .action_disabled_when(                !(has_new_changes || has_tracked_changes),                "Stash All",                StashAll.boxed_clone(),            )            .action_disabled_when(!has_stash_items, "Stash Pop", StashPop.boxed_clone())            .action("View Stash", zed_actions::git::ViewStash.boxed_clone())            .separator()            .action_disabled_when(                !has_tracked_changes,                "Discard Tracked Changes",                RestoreTrackedFiles.boxed_clone(),            )            .action_disabled_when(                !has_new_changes,                "Trash Untracked Files",                TrashUntrackedFiles.boxed_clone(),            )    })} fn git_panel_view_options_menu(    focus_handle: FocusHandle,    window: &mut Window,    cx: &mut App,) -> Entity<ContextMenu> {    let view_options_menu_state = Rc::new(Cell::new(GitPanelViewOptionsMenuState {        sort_by: GitPanelSettings::get_global(cx).sort_by,        group_by: GitPanelSettings::get_global(cx).group_by,        tree_view: GitPanelSettings::get_global(cx).tree_view,    }));     ContextMenu::build_persistent(window, cx, move |context_menu, _, _| {        let state = view_options_menu_state.get();         context_menu            .context(focus_handle.clone())            .header("View")            .item({                let view_options_menu_state = view_options_menu_state.clone();                ContextMenuEntry::new("List")                    .toggle(IconPosition::End, !state.tree_view)                    .handler(move |window, cx| {                        if state.tree_view {                            view_options_menu_state.set(GitPanelViewOptionsMenuState {                                tree_view: false,                                ..state                            });                            window.dispatch_action(Box::new(ToggleTreeView), cx);                        }                    })            })            .item({                let view_options_menu_state = view_options_menu_state.clone();                ContextMenuEntry::new("Tree")                    .toggle(IconPosition::End, state.tree_view)                    .handler(move |window, cx| {                        if !state.tree_view {                            view_options_menu_state.set(GitPanelViewOptionsMenuState {                                tree_view: true,                                ..state                            });                            window.dispatch_action(Box::new(ToggleTreeView), cx);                        }                    })            })            .when(!state.tree_view, |this| {                this.separator()                    .header("Sort By")                    .item({                        let view_options_menu_state = view_options_menu_state.clone();                        ContextMenuEntry::new("Path")                            .toggle(IconPosition::End, state.sort_by == GitPanelSortBy::Path)                            .handler(move |window, cx| {                                if !state.tree_view {                                    view_options_menu_state.set(GitPanelViewOptionsMenuState {                                        sort_by: GitPanelSortBy::Path,                                        ..state                                    });                                    window.dispatch_action(Box::new(SetSortByPath), cx);                                }                            })                    })                    .item({                        let view_options_menu_state = view_options_menu_state.clone();                        ContextMenuEntry::new("Name")                            .toggle(IconPosition::End, state.sort_by == GitPanelSortBy::Name)                            .handler(move |window, cx| {                                if !state.tree_view {                                    view_options_menu_state.set(GitPanelViewOptionsMenuState {                                        sort_by: GitPanelSortBy::Name,                                        ..state                                    });                                    window.dispatch_action(Box::new(SetSortByName), cx);                                }                            })                    })            })            .separator()            .header("Group By")            .item({                let view_options_menu_state = view_options_menu_state.clone();                ContextMenuEntry::new("None")                    .toggle(IconPosition::End, state.group_by == GitPanelGroupBy::None)                    .handler(move |window, cx| {                        if state.group_by != GitPanelGroupBy::None {                            view_options_menu_state.set(GitPanelViewOptionsMenuState {                                group_by: GitPanelGroupBy::None,                                ..state                            });                            window.dispatch_action(Box::new(SetGroupByNone), cx);                        }                    })            })            .item({                let view_options_menu_state = view_options_menu_state.clone();                ContextMenuEntry::new("Tracked & Untracked")                    .toggle(IconPosition::End, state.group_by == GitPanelGroupBy::Status)                    .handler(move |window, cx| {                        if state.group_by != GitPanelGroupBy::Status {                            view_options_menu_state.set(GitPanelViewOptionsMenuState {                                group_by: GitPanelGroupBy::Status,                                ..state                            });                            window.dispatch_action(Box::new(SetGroupByStatus), cx);                        }                    })            })            .item({                let view_options_menu_state = view_options_menu_state.clone();                ContextMenuEntry::new("Staged & Unstaged")                    .toggle(                        IconPosition::End,                        state.group_by == GitPanelGroupBy::Staging,                    )                    .handler(move |window, cx| {                        if state.group_by != GitPanelGroupBy::Staging {                            view_options_menu_state.set(GitPanelViewOptionsMenuState {                                group_by: GitPanelGroupBy::Staging,                                ..state                            });                            window.dispatch_action(Box::new(SetGroupByStaging), cx);                        }                    })            })    })} // We only allow a single remote operation at a time to avoid concurrent// credential prompts and competing ref/working-tree updates.#[derive(Clone, Copy)]pub(crate) enum RemoteOperationKind {    Fetch,    Pull,    Push,} pub fn register(workspace: &mut Workspace) {    workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {        workspace.toggle_panel_focus::<GitPanel>(window, cx);    });    workspace.register_action(|workspace, _: &Toggle, window, cx| {        if !workspace.toggle_panel_focus::<GitPanel>(window, cx) {            workspace.close_panel::<GitPanel>(window, cx);        }    });    workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {        CommitModal::toggle(workspace, None, window, cx)    });    workspace.register_action(|workspace, _: &ToggleFillCommitEditor, window, cx| {        if let Some(panel) = workspace.panel::<GitPanel>(cx) {            panel.update(cx, |panel, cx| {                panel.toggle_fill_commit_editor(&Default::default(), window, cx)            });        }    });    workspace.register_action(|workspace, _: &git::Init, window, cx| {        if let Some(panel) = workspace.panel::<GitPanel>(cx) {            panel.update(cx, |panel, cx| panel.git_init(window, cx));        }    });    workspace.register_action(|workspace, _: &ShowGitJobQueue, window, cx| {        if let Some(panel) = workspace.panel::<GitPanel>(cx) {            panel.update(cx, |panel, cx| {                panel.show_git_job_queue(window, cx);            });        }    });} #[derive(Debug, Clone)]pub enum Event {    Focus,} #[derive(Default, Serialize, Deserialize)]struct SerializedGitPanel {    #[serde(default)]    signoff_enabled: bool,    #[serde(default)]    commit_messages: BTreeMap<String, SerializedCommitMessage>,} #[derive(Clone, Default, Serialize, Deserialize)]struct SerializedCommitMessage {    #[serde(default, skip_serializing_if = "Option::is_none")]    message: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    original_message: Option<String>,    #[serde(default)]    amend_pending: bool,} #[derive(Debug, PartialEq, Eq, Clone, Copy)]enum GitPanelTab {    Changes,    History,} #[derive(Debug, PartialEq, Eq, Clone)]enum CommitHistory {    Loading,    /// A non-empty list can still grow on later fetches.    /// An empty list means the repository has no commits.    Loaded(Rc<[CommitHistoryEntry]>),    Error(SharedString),} fn commit_history_from_response(    entries: Rc<[CommitHistoryEntry]>,    is_loading: bool,    error: Option<SharedString>,) -> CommitHistory {    if !entries.is_empty() {        CommitHistory::Loaded(entries)    } else if let Some(error) = error {        CommitHistory::Error(error)    } else if is_loading {        CommitHistory::Loading    } else {        CommitHistory::Loaded(Rc::from([]))    }} #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]enum Section {    Conflict,    Tracked,    New,    Staged,    Unstaged,} #[derive(Debug, PartialEq, Eq, Clone)]struct GitHeaderEntry {    header: Section,} #[derive(Debug, PartialEq, Eq, Clone, Copy)]struct ProjectedChangeEntry {    section: Section,    index: usize,} /// What clicking a staging control should do.////// In the "staged & unstaged" grouping, a partially staged file appears in both the/// "Staged" and "Unstaged" sections at once, so a row's meaning comes from/// the section it is rendered in rather than from the file's own state.#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum StageIntent {    Stage,    Unstage,    Toggle,} impl StageIntent {    fn for_section(section: Section) -> Self {        match section {            Section::Staged => StageIntent::Unstage,            Section::Unstaged => StageIntent::Stage,

Lines 1–500 of 12652.