1039 lines · 33.4 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use buffer_diff::{BufferDiff, BufferDiffSnapshot};use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};use git::repository::{CommitDetails, CommitDiff, RepoPath};use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};use gpui::{    AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element,    Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,    PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,};use language::{    Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope,    TextBuffer, ToPoint,};use multi_buffer::ExcerptInfo;use multi_buffer::PathKey;use project::{Project, WorktreeId, git_store::Repository};use std::{    any::{Any, TypeId},    path::PathBuf,    sync::Arc,};use theme::ActiveTheme;use ui::{    Avatar, Button, ButtonCommon, Clickable, Color, Icon, IconName, IconSize, Label,    LabelCommon as _, LabelSize, SharedString, div, h_flex, v_flex,};use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};use workspace::{    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,    Workspace,    item::{BreadcrumbText, ItemEvent, TabContentParams},    notifications::NotifyTaskExt,    pane::SaveIntent,    searchable::SearchableItemHandle,}; use crate::git_panel::GitPanel; actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]); pub fn init(cx: &mut App) {    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {        workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {            CommitView::apply_stash(workspace, window, cx);        });        workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {            CommitView::remove_stash(workspace, window, cx);        });        workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {            CommitView::pop_stash(workspace, window, cx);        });    })    .detach();} pub struct CommitView {    commit: CommitDetails,    editor: Entity<Editor>,    stash: Option<usize>,    multibuffer: Entity<MultiBuffer>,    repository: Entity<Repository>,    remote: Option<GitRemote>,} struct GitBlob {    path: RepoPath,    worktree_id: WorktreeId,    is_deleted: bool,} const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; impl CommitView {    pub fn open(        commit_sha: String,        repo: WeakEntity<Repository>,        workspace: WeakEntity<Workspace>,        stash: Option<usize>,        file_filter: Option<RepoPath>,        window: &mut Window,        cx: &mut App,    ) {        let commit_diff = repo            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))            .ok();        let commit_details = repo            .update(cx, |repo, _| repo.show(commit_sha.clone()))            .ok();         window            .spawn(cx, async move |cx| {                let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);                let mut commit_diff = commit_diff.log_err()?.log_err()?;                let commit_details = commit_details.log_err()?.log_err()?;                 // Filter to specific file if requested                if let Some(ref filter_path) = file_filter {                    commit_diff.files.retain(|f| &f.path == filter_path);                }                 let repo = repo.upgrade()?;                 workspace                    .update_in(cx, |workspace, window, cx| {                        let project = workspace.project();                        let commit_view = cx.new(|cx| {                            CommitView::new(                                commit_details,                                commit_diff,                                repo,                                project.clone(),                                stash,                                window,                                cx,                            )                        });                         let pane = workspace.active_pane();                        pane.update(cx, |pane, cx| {                            let ix = pane.items().position(|item| {                                let commit_view = item.downcast::<CommitView>();                                commit_view                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)                            });                            if let Some(ix) = ix {                                pane.activate_item(ix, true, true, window, cx);                            } else {                                pane.add_item(Box::new(commit_view), true, true, None, window, cx);                            }                        })                    })                    .log_err()            })            .detach();    }     fn new(        commit: CommitDetails,        commit_diff: CommitDiff,        repository: Entity<Repository>,        project: Entity<Project>,        stash: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let language_registry = project.read(cx).languages().clone();        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));        let editor = cx.new(|cx| {            let mut editor =                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);            editor.disable_inline_diagnostics();            editor.set_expand_all_diff_hunks(cx);            editor.register_addon(CommitViewAddon {                multibuffer: multibuffer.downgrade(),            });            editor        });         let first_worktree_id = project            .read(cx)            .worktrees(cx)            .next()            .map(|worktree| worktree.read(cx).id());         let repository_clone = repository.clone();        let commit_message = commit.message.clone();         cx.spawn(async move |this, cx| {            for file in commit_diff.files {                let is_deleted = file.new_text.is_none();                let new_text = file.new_text.unwrap_or_default();                let old_text = file.old_text;                let worktree_id = repository_clone                    .update(cx, |repository, cx| {                        repository                            .repo_path_to_project_path(&file.path, cx)                            .map(|path| path.worktree_id)                            .or(first_worktree_id)                    })?                    .context("project has no worktrees")?;                let file = Arc::new(GitBlob {                    path: file.path.clone(),                    is_deleted,                    worktree_id,                }) as Arc<dyn language::File>;                 let buffer = build_buffer(new_text, file, &language_registry, cx).await?;                let buffer_diff =                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;                 this.update(cx, |this, cx| {                    this.multibuffer.update(cx, |multibuffer, cx| {                        let snapshot = buffer.read(cx).snapshot();                        let path = snapshot.file().unwrap().path().clone();                         let hunks: Vec<_> = buffer_diff.read(cx).hunks(&snapshot, cx).collect();                         let excerpt_ranges = if hunks.is_empty() {                            vec![language::Point::zero()..snapshot.max_point()]                        } else {                            hunks                                .into_iter()                                .map(|hunk| {                                    let start = hunk.range.start.max(language::Point::new(                                        hunk.range.start.row.saturating_sub(3),                                        0,                                    ));                                    let end_row =                                        (hunk.range.end.row + 3).min(snapshot.max_point().row);                                    let end =                                        language::Point::new(end_row, snapshot.line_len(end_row));                                    start..end                                })                                .collect()                        };                         let _is_newly_added = multibuffer.set_excerpts_for_path(                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),                            buffer,                            excerpt_ranges,                            0,                            cx,                        );                        multibuffer.add_diff(buffer_diff, cx);                    });                })?;            }             let message_buffer = cx.new(|cx| {                let mut buffer = Buffer::local(commit_message, cx);                buffer.set_capability(Capability::ReadOnly, cx);                buffer            })?;             this.update(cx, |this, cx| {                this.multibuffer.update(cx, |multibuffer, cx| {                    let range = ExcerptRange {                        context: Anchor::MIN..Anchor::MAX,                        primary: Anchor::MIN..Anchor::MAX,                    };                    multibuffer.insert_excerpts_after(                        ExcerptId::min(),                        message_buffer.clone(),                        [range],                        cx,                    )                });                 this.editor.update(cx, |editor, cx| {                    editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);                     editor.insert_blocks(                        [BlockProperties {                            placement: BlockPlacement::Above(editor::Anchor::min()),                            height: Some(1),                            style: BlockStyle::Sticky,                            render: Arc::new(|_| gpui::Empty.into_any_element()),                            priority: 0,                        }]                        .into_iter()                        .chain(                            editor                                .buffer()                                .read(cx)                                .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)                                .map(|anchor| BlockProperties {                                    placement: BlockPlacement::Below(anchor),                                    height: Some(1),                                    style: BlockStyle::Sticky,                                    render: Arc::new(|_| gpui::Empty.into_any_element()),                                    priority: 0,                                }),                        ),                        None,                        cx,                    )                });            })?;             anyhow::Ok(())        })        .detach();         let snapshot = repository.read(cx).snapshot();        let remote_url = snapshot            .remote_upstream_url            .as_ref()            .or(snapshot.remote_origin_url.as_ref());         let remote = remote_url.and_then(|url| {            let provider_registry = GitHostingProviderRegistry::default_global(cx);            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {                host,                owner: parsed.owner.into(),                repo: parsed.repo.into(),            })        });         Self {            commit,            editor,            multibuffer,            stash,            repository,            remote,        }    }     fn render_commit_avatar(        &self,        sha: &SharedString,        size: impl Into<gpui::AbsoluteLength>,        window: &mut Window,        cx: &mut App,    ) -> AnyElement {        let size = size.into();        let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());         if let Some(remote) = remote {            let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());            if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {                return Avatar::new(url.to_string())                    .size(size)                    .into_element()                    .into_any();            }        }         v_flex()            .w(size)            .h(size)            .border_1()            .border_color(cx.theme().colors().border)            .rounded_full()            .justify_center()            .items_center()            .child(                Icon::new(IconName::Person)                    .color(Color::Muted)                    .size(IconSize::Medium)                    .into_element(),            )            .into_any()    }     fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let commit = &self.commit;        let author_name = commit.author_name.clone();        let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)            .unwrap_or_else(|_| time::OffsetDateTime::now_utc());        let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);        let date_string = time_format::format_localized_timestamp(            commit_date,            time::OffsetDateTime::now_utc(),            local_offset,            time_format::TimestampFormat::MediumAbsolute,        );         let github_url = self.remote.as_ref().map(|remote| {            format!(                "{}/{}/{}/commit/{}",                remote.host.base_url(),                remote.owner,                remote.repo,                commit.sha            )        });         v_flex()            .p_4()            .pl_0()            .gap_4()            .border_b_1()            .border_color(cx.theme().colors().border)            .child(                h_flex()                    .items_start()                    .child(                        h_flex()                            .w(self.editor.read(cx).last_gutter_dimensions().full_width())                            .justify_center()                            .child(self.render_commit_avatar(                                &commit.sha,                                gpui::rems(3.0),                                window,                                cx,                            )),                    )                    .child(                        v_flex()                            .gap_1()                            .child(                                h_flex()                                    .gap_3()                                    .items_baseline()                                    .child(Label::new(author_name).color(Color::Default))                                    .child(                                        Label::new(format!("commit {}", commit.sha))                                            .color(Color::Muted),                                    ),                            )                            .child(Label::new(date_string).color(Color::Muted)),                    )                    .child(div().flex_grow())                    .children(github_url.map(|url| {                        Button::new("view_on_github", "View on GitHub")                            .icon(IconName::Github)                            .style(ui::ButtonStyle::Subtle)                            .on_click(move |_, _, cx| cx.open_url(&url))                    })),            )    }     fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {        Self::stash_action(            workspace,            "Apply",            window,            cx,            async move |repository, sha, stash, commit_view, workspace, cx| {                let result = repository.update(cx, |repo, cx| {                    if !stash_matches_index(&sha, stash, repo) {                        return Err(anyhow::anyhow!("Stash has changed, not applying"));                    }                    Ok(repo.stash_apply(Some(stash), cx))                })?;                 match result {                    Ok(task) => task.await?,                    Err(err) => {                        Self::close_commit_view(commit_view, workspace, cx).await?;                        return Err(err);                    }                };                Self::close_commit_view(commit_view, workspace, cx).await?;                anyhow::Ok(())            },        );    }     fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {        Self::stash_action(            workspace,            "Pop",            window,            cx,            async move |repository, sha, stash, commit_view, workspace, cx| {                let result = repository.update(cx, |repo, cx| {                    if !stash_matches_index(&sha, stash, repo) {                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));                    }                    Ok(repo.stash_pop(Some(stash), cx))                })?;                 match result {                    Ok(task) => task.await?,                    Err(err) => {                        Self::close_commit_view(commit_view, workspace, cx).await?;                        return Err(err);                    }                };                Self::close_commit_view(commit_view, workspace, cx).await?;                anyhow::Ok(())            },        );    }     fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {        Self::stash_action(            workspace,            "Drop",            window,            cx,            async move |repository, sha, stash, commit_view, workspace, cx| {                let result = repository.update(cx, |repo, cx| {                    if !stash_matches_index(&sha, stash, repo) {                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));                    }                    Ok(repo.stash_drop(Some(stash), cx))                })?;                 match result {                    Ok(task) => task.await??,                    Err(err) => {                        Self::close_commit_view(commit_view, workspace, cx).await?;                        return Err(err);                    }                };                Self::close_commit_view(commit_view, workspace, cx).await?;                anyhow::Ok(())            },        );    }     fn stash_action<AsyncFn>(        workspace: &mut Workspace,        str_action: &str,        window: &mut Window,

Lines 1–500 of 1039.