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, 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, 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, _ => StageIntent::Toggle, } }Lines 1–500 of 12280. Load more
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, 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, 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 panel const 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, _ => StageIntent::Toggle, } }