Cole Millerwip40000c48 months agoHistory
1090 lines · 36.4 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use buffer_diff::BufferDiff;use collections::HashMap;use editor::{    Addon, Editor, EditorEvent, EditorSettings, MultiBuffer, SelectionEffects, SplittableEditor,    multibuffer_context_lines, scroll::Autoscroll,};use git::repository::{CommitDetails, CommitDiff, RepoPath, is_binary_content};use git::status::{FileStatus, StatusCode, TrackedStatus};use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};use gpui::{    Action, AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Entity,    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,    PromptLevel, Render, SharedString, Styled, Task, WeakEntity, Window, actions,};use language::{    Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,    ReplicaId, Rope, TextBuffer,};use multi_buffer::PathKey;use project::{Project, WorktreeId, git_store::Repository};use settings::Settings;use std::{    any::{Any, TypeId},    collections::HashSet,    path::PathBuf,    sync::Arc,};use theme::ActiveTheme;use ui::{Avatar, ButtonLike, CopyButton, Tooltip, prelude::*};use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};use workspace::item::TabTooltipContent;use workspace::{    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,    Workspace,    item::{ItemEvent, TabContentParams},    notifications::NotifyTaskExt,    pane::SaveIntent,    searchable::SearchableItemHandle,}; use crate::commit_details_sidebar::{    CommitDetailsSidebar, CommitDetailsSidebarData, get_remote_from_repository,};use crate::commit_tooltip::CommitAvatarAsset;use crate::git_panel::GitPanel; actions!(    git,    [        ApplyCurrentStash,        PopCurrentStash,        DropCurrentStash,        ToggleCommitDetailsSidebar,    ]); 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);        });        workspace.register_action(|workspace, _: &ToggleCommitDetailsSidebar, window, cx| {            CommitView::toggle_details_sidebar(workspace, window, cx);        });    })    .detach();} pub struct CommitView {    commit: CommitDetails,    editor: Entity<SplittableEditor>,    stash: Option<usize>,    multibuffer: Entity<MultiBuffer>,    repository: Entity<Repository>,    workspace: WeakEntity<Workspace>,    remote: Option<GitRemote>,    changed_files: Vec<(RepoPath, FileStatus)>,    show_details_sidebar: bool,} struct GitBlob {    path: RepoPath,    worktree_id: WorktreeId,    is_deleted: bool,    is_binary: bool,    display_name: String,} struct CommitDiffAddon {    file_statuses: HashMap<language::BufferId, FileStatus>,} impl Addon for CommitDiffAddon {    fn to_any(&self) -> &dyn std::any::Any {        self    }     fn override_status_for_buffer_id(        &self,        buffer_id: language::BufferId,        _cx: &App,    ) -> Option<FileStatus> {        self.file_statuses.get(&buffer_id).copied()    }} 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 workspace_handle = cx.entity();                        let commit_view = cx.new(|cx| {                            CommitView::new(                                commit_details,                                commit_diff,                                repo,                                project.clone(),                                workspace_handle,                                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>,        workspace: Entity<Workspace>,        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 changed_files: Vec<(RepoPath, FileStatus)> = commit_diff            .files            .iter()            .map(|file| {                let is_created = file.old_text.is_none();                let is_deleted = file.new_text.is_none();                let status_code = if is_created {                    StatusCode::Added                } else if is_deleted {                    StatusCode::Deleted                } else {                    StatusCode::Modified                };                (                    file.path.clone(),                    FileStatus::Tracked(TrackedStatus {                        index_status: status_code,                        worktree_status: StatusCode::Unmodified,                    }),                )            })            .collect();         let editor = cx.new(|cx| {            let editor = SplittableEditor::new(                EditorSettings::get_global(cx).diff_view_style,                multibuffer.clone(),                project.clone(),                workspace.clone(),                window,                cx,            );            editor.rhs_editor().update(cx, |editor, cx| {                editor.disable_inline_diagnostics();                editor.set_show_breakpoints(false, cx);                editor.set_show_diff_review_button(true, cx);            });            editor        });         let commit_sha = Arc::<str>::from(commit.sha.as_ref());         let first_worktree_id = project            .read(cx)            .worktrees(cx)            .next()            .map(|worktree| worktree.read(cx).id());         let repository_clone = repository.clone();         cx.spawn(async move |this, cx| {            let mut binary_buffer_ids: HashSet<language::BufferId> = HashSet::default();            let mut file_statuses: HashMap<language::BufferId, FileStatus> = HashMap::default();             for file in commit_diff.files {                let is_created = file.old_text.is_none();                let is_deleted = file.new_text.is_none();                let raw_new_text = file.new_text.unwrap_or_default();                let raw_old_text = file.old_text;                 let is_binary = file.is_binary                    || is_binary_content(raw_new_text.as_bytes())                    || raw_old_text                        .as_ref()                        .is_some_and(|text| is_binary_content(text.as_bytes()));                 let new_text = if is_binary {                    "(binary file not shown)".to_string()                } else {                    raw_new_text                };                let old_text = if is_binary {                    Some(new_text.clone())                } else {                    raw_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 short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha);                let file_name = file                    .path                    .file_name()                    .map(|name| name.to_string())                    .unwrap_or_else(|| file.path.display(PathStyle::local()).to_string());                let display_name = format!("{short_sha} - {file_name}");                 let file = Arc::new(GitBlob {                    path: file.path.clone(),                    is_deleted,                    is_binary,                    worktree_id,                    display_name,                }) as Arc<dyn language::File>;                 let buffer = build_buffer(new_text, file, &language_registry, cx).await?;                let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());                 let status_code = if is_created {                    StatusCode::Added                } else if is_deleted {                    StatusCode::Deleted                } else {                    StatusCode::Modified                };                file_statuses.insert(                    buffer_id,                    FileStatus::Tracked(TrackedStatus {                        index_status: status_code,                        worktree_status: StatusCode::Unmodified,                    }),                );                 if is_binary {                    binary_buffer_ids.insert(buffer_id);                }                 let buffer_diff =                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;                 this.update(cx, |this, cx| {                    let snapshot = buffer.read(cx).snapshot();                    let path = snapshot.file().unwrap().path().clone();                    let path_key = PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path);                     let diff_snapshot = buffer_diff.read(cx).snapshot(cx);                    let mut hunks = diff_snapshot.hunks(&snapshot).peekable();                    let excerpt_ranges = if hunks.peek().is_none() {                        vec![language::Point::zero()..snapshot.max_point()]                    } else {                        hunks                            .map(|hunk| hunk.buffer_range.to_point(&snapshot))                            .collect::<Vec<_>>()                    };                     this.editor.update(cx, |editor, cx| {                        editor.set_excerpts_for_path(                            path_key,                            buffer,                            excerpt_ranges,                            multibuffer_context_lines(cx),                            buffer_diff,                            cx,                        );                    });                })?;            }             this.update(cx, |this, cx| {                this.editor.update(cx, |editor, cx| {                    editor.rhs_editor().update(cx, |rhs_editor, _cx| {                        rhs_editor.register_addon(CommitDiffAddon { file_statuses });                    });                });                if !binary_buffer_ids.is_empty() {                    this.editor.update(cx, |editor, cx| {                        editor.rhs_editor().update(cx, |rhs_editor, cx| {                            rhs_editor.fold_buffers(binary_buffer_ids, 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,            workspace: workspace.downgrade(),            remote,            changed_files,            show_details_sidebar: false,        }    }     pub fn toggle_details_sidebar(workspace: &mut Workspace, _window: &mut Window, cx: &mut App) {        if let Some(commit_view) = workspace            .active_item(cx)            .and_then(|item| item.act_as::<CommitView>(cx))        {            commit_view.update(cx, |this, cx| {                this.show_details_sidebar = !this.show_details_sidebar;                cx.notify();            });        }    }     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,        cx: &mut App,        callback: AsyncFn,    ) where        AsyncFn: AsyncFnOnce(                Entity<Repository>,                &SharedString,                usize,                Entity<CommitView>,                WeakEntity<Workspace>,

Lines 1–500 of 1090.