commit_view.rs

   1use anyhow::{Context as _, Result};
   2use buffer_diff::BufferDiff;
   3use collections::HashMap;
   4use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
   5use editor::{Addon, Editor, EditorEvent, ExcerptRange, MultiBuffer, multibuffer_context_lines};
   6use git::repository::{CommitDetails, CommitDiff, RepoPath, is_binary_content};
   7use git::status::{FileStatus, StatusCode, TrackedStatus};
   8use git::{
   9    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, ParsedGitRemote,
  10    parse_git_remote_url,
  11};
  12use gpui::{
  13    AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context,
  14    Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
  15    ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
  16};
  17use language::{
  18    Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
  19    Point, ReplicaId, Rope, TextBuffer,
  20};
  21use multi_buffer::PathKey;
  22use project::{Project, WorktreeId, git_store::Repository};
  23use std::{
  24    any::{Any, TypeId},
  25    collections::HashSet,
  26    path::PathBuf,
  27    sync::Arc,
  28};
  29use theme::ActiveTheme;
  30use ui::{ButtonLike, DiffStat, Tooltip, prelude::*};
  31use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
  32use workspace::item::TabTooltipContent;
  33use workspace::{
  34    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  35    Workspace,
  36    item::{ItemEvent, TabContentParams},
  37    notifications::NotifyTaskExt,
  38    pane::SaveIntent,
  39    searchable::SearchableItemHandle,
  40};
  41
  42use crate::commit_tooltip::CommitAvatar;
  43use crate::git_panel::GitPanel;
  44
  45actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
  46
  47pub fn init(cx: &mut App) {
  48    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
  49        workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {
  50            CommitView::apply_stash(workspace, window, cx);
  51        });
  52        workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {
  53            CommitView::remove_stash(workspace, window, cx);
  54        });
  55        workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {
  56            CommitView::pop_stash(workspace, window, cx);
  57        });
  58    })
  59    .detach();
  60}
  61
  62pub struct CommitView {
  63    commit: CommitDetails,
  64    editor: Entity<Editor>,
  65    stash: Option<usize>,
  66    multibuffer: Entity<MultiBuffer>,
  67    repository: Entity<Repository>,
  68    remote: Option<GitRemote>,
  69}
  70
  71struct GitBlob {
  72    path: RepoPath,
  73    worktree_id: WorktreeId,
  74    is_deleted: bool,
  75    is_binary: bool,
  76    display_name: String,
  77}
  78
  79struct CommitDiffAddon {
  80    file_statuses: HashMap<language::BufferId, FileStatus>,
  81}
  82
  83impl Addon for CommitDiffAddon {
  84    fn to_any(&self) -> &dyn std::any::Any {
  85        self
  86    }
  87
  88    fn override_status_for_buffer_id(
  89        &self,
  90        buffer_id: language::BufferId,
  91        _cx: &App,
  92    ) -> Option<FileStatus> {
  93        self.file_statuses.get(&buffer_id).copied()
  94    }
  95}
  96
  97const COMMIT_MESSAGE_SORT_PREFIX: u64 = 0;
  98const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
  99
 100impl CommitView {
 101    pub fn open(
 102        commit_sha: String,
 103        repo: WeakEntity<Repository>,
 104        workspace: WeakEntity<Workspace>,
 105        stash: Option<usize>,
 106        file_filter: Option<RepoPath>,
 107        window: &mut Window,
 108        cx: &mut App,
 109    ) {
 110        let commit_diff = repo
 111            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
 112            .ok();
 113        let commit_details = repo
 114            .update(cx, |repo, _| repo.show(commit_sha.clone()))
 115            .ok();
 116
 117        window
 118            .spawn(cx, async move |cx| {
 119                let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
 120                let mut commit_diff = commit_diff.log_err()?.log_err()?;
 121                let commit_details = commit_details.log_err()?.log_err()?;
 122
 123                // Filter to specific file if requested
 124                if let Some(ref filter_path) = file_filter {
 125                    commit_diff.files.retain(|f| &f.path == filter_path);
 126                }
 127
 128                let repo = repo.upgrade()?;
 129
 130                workspace
 131                    .update_in(cx, |workspace, window, cx| {
 132                        let project = workspace.project();
 133                        let commit_view = cx.new(|cx| {
 134                            CommitView::new(
 135                                commit_details,
 136                                commit_diff,
 137                                repo,
 138                                project.clone(),
 139                                stash,
 140                                window,
 141                                cx,
 142                            )
 143                        });
 144
 145                        let pane = workspace.active_pane();
 146                        pane.update(cx, |pane, cx| {
 147                            let ix = pane.items().position(|item| {
 148                                let commit_view = item.downcast::<CommitView>();
 149                                commit_view
 150                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
 151                            });
 152                            if let Some(ix) = ix {
 153                                pane.activate_item(ix, true, true, window, cx);
 154                            } else {
 155                                pane.add_item(Box::new(commit_view), true, true, None, window, cx);
 156                            }
 157                        })
 158                    })
 159                    .log_err()
 160            })
 161            .detach();
 162    }
 163
 164    fn new(
 165        commit: CommitDetails,
 166        commit_diff: CommitDiff,
 167        repository: Entity<Repository>,
 168        project: Entity<Project>,
 169        stash: Option<usize>,
 170        window: &mut Window,
 171        cx: &mut Context<Self>,
 172    ) -> Self {
 173        let language_registry = project.read(cx).languages().clone();
 174        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
 175
 176        let message_buffer = cx.new(|cx| {
 177            let mut buffer = Buffer::local(commit.message.clone(), cx);
 178            buffer.set_capability(Capability::ReadOnly, cx);
 179            buffer
 180        });
 181
 182        multibuffer.update(cx, |multibuffer, cx| {
 183            let snapshot = message_buffer.read(cx).snapshot();
 184            let full_range = Point::zero()..snapshot.max_point();
 185            let range = ExcerptRange {
 186                context: full_range.clone(),
 187                primary: full_range,
 188            };
 189            multibuffer.set_excerpt_ranges_for_path(
 190                PathKey::with_sort_prefix(
 191                    COMMIT_MESSAGE_SORT_PREFIX,
 192                    RelPath::unix("commit message").unwrap().into(),
 193                ),
 194                message_buffer.clone(),
 195                &snapshot,
 196                vec![range],
 197                cx,
 198            )
 199        });
 200
 201        let editor = cx.new(|cx| {
 202            let mut editor =
 203                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 204
 205            editor.disable_inline_diagnostics();
 206            editor.set_show_breakpoints(false, cx);
 207            editor.set_expand_all_diff_hunks(cx);
 208            editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
 209            editor.disable_indent_guides_for_buffer(message_buffer.read(cx).remote_id(), cx);
 210
 211            editor.insert_blocks(
 212                [BlockProperties {
 213                    placement: BlockPlacement::Above(editor::Anchor::min()),
 214                    height: Some(1),
 215                    style: BlockStyle::Sticky,
 216                    render: Arc::new(|_| gpui::Empty.into_any_element()),
 217                    priority: 0,
 218                }]
 219                .into_iter()
 220                .chain(
 221                    editor
 222                        .buffer()
 223                        .read(cx)
 224                        .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
 225                        .map(|anchor| BlockProperties {
 226                            placement: BlockPlacement::Below(anchor),
 227                            height: Some(1),
 228                            style: BlockStyle::Sticky,
 229                            render: Arc::new(|_| gpui::Empty.into_any_element()),
 230                            priority: 0,
 231                        }),
 232                ),
 233                None,
 234                cx,
 235            );
 236
 237            editor
 238        });
 239
 240        let commit_sha = Arc::<str>::from(commit.sha.as_ref());
 241
 242        let first_worktree_id = project
 243            .read(cx)
 244            .worktrees(cx)
 245            .next()
 246            .map(|worktree| worktree.read(cx).id());
 247
 248        let repository_clone = repository.clone();
 249
 250        cx.spawn(async move |this, cx| {
 251            let mut binary_buffer_ids: HashSet<language::BufferId> = HashSet::default();
 252            let mut file_statuses: HashMap<language::BufferId, FileStatus> = HashMap::default();
 253
 254            for file in commit_diff.files {
 255                let is_created = file.old_text.is_none();
 256                let is_deleted = file.new_text.is_none();
 257                let raw_new_text = file.new_text.unwrap_or_default();
 258                let raw_old_text = file.old_text;
 259
 260                let is_binary = file.is_binary
 261                    || is_binary_content(raw_new_text.as_bytes())
 262                    || raw_old_text
 263                        .as_ref()
 264                        .is_some_and(|text| is_binary_content(text.as_bytes()));
 265
 266                let new_text = if is_binary {
 267                    "(binary file not shown)".to_string()
 268                } else {
 269                    raw_new_text
 270                };
 271                let old_text = if is_binary { None } else { raw_old_text };
 272                let worktree_id = repository_clone
 273                    .update(cx, |repository, cx| {
 274                        repository
 275                            .repo_path_to_project_path(&file.path, cx)
 276                            .map(|path| path.worktree_id)
 277                            .or(first_worktree_id)
 278                    })
 279                    .context("project has no worktrees")?;
 280                let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha);
 281                let file_name = file
 282                    .path
 283                    .file_name()
 284                    .map(|name| name.to_string())
 285                    .unwrap_or_else(|| file.path.display(PathStyle::local()).to_string());
 286                let display_name = format!("{short_sha} - {file_name}");
 287
 288                let file = Arc::new(GitBlob {
 289                    path: file.path.clone(),
 290                    is_deleted,
 291                    is_binary,
 292                    worktree_id,
 293                    display_name,
 294                }) as Arc<dyn language::File>;
 295
 296                let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
 297                let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
 298
 299                let status_code = if is_created {
 300                    StatusCode::Added
 301                } else if is_deleted {
 302                    StatusCode::Deleted
 303                } else {
 304                    StatusCode::Modified
 305                };
 306                file_statuses.insert(
 307                    buffer_id,
 308                    FileStatus::Tracked(TrackedStatus {
 309                        index_status: status_code,
 310                        worktree_status: StatusCode::Unmodified,
 311                    }),
 312                );
 313
 314                if is_binary {
 315                    binary_buffer_ids.insert(buffer_id);
 316                }
 317
 318                let buffer_diff = if is_binary {
 319                    None
 320                } else {
 321                    Some(build_buffer_diff(old_text, &buffer, &language_registry, cx).await?)
 322                };
 323
 324                this.update(cx, |this, cx| {
 325                    this.multibuffer.update(cx, |multibuffer, cx| {
 326                        let snapshot = buffer.read(cx).snapshot();
 327                        let path = snapshot.file().unwrap().path().clone();
 328                        let excerpt_ranges = if is_binary {
 329                            vec![language::Point::zero()..snapshot.max_point()]
 330                        } else if let Some(buffer_diff) = &buffer_diff {
 331                            let diff_snapshot = buffer_diff.read(cx).snapshot(cx);
 332                            let mut hunks = diff_snapshot.hunks(&snapshot).peekable();
 333                            if hunks.peek().is_none() {
 334                                vec![language::Point::zero()..snapshot.max_point()]
 335                            } else {
 336                                hunks
 337                                    .map(|hunk| hunk.buffer_range.to_point(&snapshot))
 338                                    .collect::<Vec<_>>()
 339                            }
 340                        } else {
 341                            vec![language::Point::zero()..snapshot.max_point()]
 342                        };
 343
 344                        let _is_newly_added = multibuffer.set_excerpts_for_path(
 345                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
 346                            buffer,
 347                            excerpt_ranges,
 348                            multibuffer_context_lines(cx),
 349                            cx,
 350                        );
 351                        if let Some(buffer_diff) = buffer_diff {
 352                            multibuffer.add_diff(buffer_diff, cx);
 353                        }
 354                    });
 355                })?;
 356            }
 357
 358            this.update(cx, |this, cx| {
 359                this.editor.update(cx, |editor, _cx| {
 360                    editor.register_addon(CommitDiffAddon { file_statuses });
 361                });
 362                if !binary_buffer_ids.is_empty() {
 363                    this.editor.update(cx, |editor, cx| {
 364                        for buffer_id in binary_buffer_ids {
 365                            editor.fold_buffer(buffer_id, cx);
 366                        }
 367                    });
 368                }
 369            })?;
 370
 371            anyhow::Ok(())
 372        })
 373        .detach();
 374
 375        let snapshot = repository.read(cx).snapshot();
 376        let remote_url = snapshot
 377            .remote_upstream_url
 378            .as_ref()
 379            .or(snapshot.remote_origin_url.as_ref());
 380
 381        let remote = remote_url.and_then(|url| {
 382            let provider_registry = GitHostingProviderRegistry::default_global(cx);
 383            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
 384                host,
 385                owner: parsed.owner.into(),
 386                repo: parsed.repo.into(),
 387            })
 388        });
 389
 390        Self {
 391            commit,
 392            editor,
 393            multibuffer,
 394            stash,
 395            repository,
 396            remote,
 397        }
 398    }
 399
 400    fn render_commit_avatar(
 401        &self,
 402        sha: &SharedString,
 403        size: impl Into<gpui::AbsoluteLength>,
 404        window: &mut Window,
 405        cx: &mut App,
 406    ) -> AnyElement {
 407        let size = size.into();
 408        let avatar = CommitAvatar::new(sha, self.remote.as_ref());
 409
 410        v_flex()
 411            .w(size)
 412            .h(size)
 413            .border_1()
 414            .border_color(cx.theme().colors().border)
 415            .rounded_full()
 416            .justify_center()
 417            .items_center()
 418            .child(
 419                avatar
 420                    .avatar(window, cx)
 421                    .map(|a| a.size(size).into_any_element())
 422                    .unwrap_or_else(|| {
 423                        Icon::new(IconName::Person)
 424                            .color(Color::Muted)
 425                            .size(IconSize::Medium)
 426                            .into_any_element()
 427                    }),
 428            )
 429            .into_any()
 430    }
 431
 432    fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
 433        let snapshot = self.multibuffer.read(cx).snapshot(cx);
 434        let mut total_additions = 0u32;
 435        let mut total_deletions = 0u32;
 436
 437        let mut seen_buffers = std::collections::HashSet::new();
 438        for (_, buffer, _) in snapshot.excerpts() {
 439            let buffer_id = buffer.remote_id();
 440            if !seen_buffers.insert(buffer_id) {
 441                continue;
 442            }
 443
 444            let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
 445                continue;
 446            };
 447
 448            let base_text = diff.base_text();
 449
 450            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
 451                let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
 452                total_additions += added_rows;
 453
 454                let base_start = base_text
 455                    .offset_to_point(hunk.diff_base_byte_range.start)
 456                    .row;
 457                let base_end = base_text.offset_to_point(hunk.diff_base_byte_range.end).row;
 458                let deleted_rows = base_end.saturating_sub(base_start);
 459
 460                total_deletions += deleted_rows;
 461            }
 462        }
 463
 464        (total_additions, total_deletions)
 465    }
 466
 467    fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 468        let commit = &self.commit;
 469        let author_name = commit.author_name.clone();
 470        let commit_sha = commit.sha.clone();
 471        let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
 472            .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
 473        let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
 474        let date_string = time_format::format_localized_timestamp(
 475            commit_date,
 476            time::OffsetDateTime::now_utc(),
 477            local_offset,
 478            time_format::TimestampFormat::MediumAbsolute,
 479        );
 480
 481        let remote_info = self.remote.as_ref().map(|remote| {
 482            let provider = remote.host.name();
 483            let parsed_remote = ParsedGitRemote {
 484                owner: remote.owner.as_ref().into(),
 485                repo: remote.repo.as_ref().into(),
 486            };
 487            let params = BuildCommitPermalinkParams { sha: &commit.sha };
 488            let url = remote
 489                .host
 490                .build_commit_permalink(&parsed_remote, params)
 491                .to_string();
 492            (provider, url)
 493        });
 494
 495        let (additions, deletions) = self.calculate_changed_lines(cx);
 496
 497        let commit_diff_stat = if additions > 0 || deletions > 0 {
 498            Some(DiffStat::new(
 499                "commit-diff-stat",
 500                additions as usize,
 501                deletions as usize,
 502            ))
 503        } else {
 504            None
 505        };
 506
 507        let gutter_width = self.editor.update(cx, |editor, cx| {
 508            let snapshot = editor.snapshot(window, cx);
 509            let style = editor.style(cx);
 510            let font_id = window.text_system().resolve_font(&style.text.font());
 511            let font_size = style.text.font_size.to_pixels(window.rem_size());
 512            snapshot
 513                .gutter_dimensions(font_id, font_size, style, window, cx)
 514                .full_width()
 515        });
 516
 517        let clipboard_has_link = cx
 518            .read_from_clipboard()
 519            .and_then(|entry| entry.text())
 520            .map_or(false, |clipboard_text| {
 521                clipboard_text.trim() == commit_sha.as_ref()
 522            });
 523
 524        let (copy_icon, copy_icon_color) = if clipboard_has_link {
 525            (IconName::Check, Color::Success)
 526        } else {
 527            (IconName::Copy, Color::Muted)
 528        };
 529
 530        h_flex()
 531            .border_b_1()
 532            .border_color(cx.theme().colors().border_variant)
 533            .w_full()
 534            .child(
 535                h_flex()
 536                    .w(gutter_width)
 537                    .justify_center()
 538                    .child(self.render_commit_avatar(&commit.sha, rems_from_px(48.), window, cx)),
 539            )
 540            .child(
 541                h_flex()
 542                    .py_4()
 543                    .pl_1()
 544                    .pr_4()
 545                    .w_full()
 546                    .items_start()
 547                    .justify_between()
 548                    .flex_wrap()
 549                    .child(
 550                        v_flex()
 551                            .child(
 552                                h_flex()
 553                                    .gap_1()
 554                                    .child(Label::new(author_name).color(Color::Default))
 555                                    .child({
 556                                        ButtonLike::new("sha")
 557                                            .child(
 558                                                h_flex()
 559                                                    .group("sha_btn")
 560                                                    .size_full()
 561                                                    .max_w_32()
 562                                                    .gap_0p5()
 563                                                    .child(
 564                                                        Label::new(commit_sha.clone())
 565                                                            .color(Color::Muted)
 566                                                            .size(LabelSize::Small)
 567                                                            .truncate()
 568                                                            .buffer_font(cx),
 569                                                    )
 570                                                    .child(
 571                                                        div().visible_on_hover("sha_btn").child(
 572                                                            Icon::new(copy_icon)
 573                                                                .color(copy_icon_color)
 574                                                                .size(IconSize::Small),
 575                                                        ),
 576                                                    ),
 577                                            )
 578                                            .tooltip({
 579                                                let commit_sha = commit_sha.clone();
 580                                                move |_, cx| {
 581                                                    Tooltip::with_meta(
 582                                                        "Copy Commit SHA",
 583                                                        None,
 584                                                        commit_sha.clone(),
 585                                                        cx,
 586                                                    )
 587                                                }
 588                                            })
 589                                            .on_click(move |_, _, cx| {
 590                                                cx.stop_propagation();
 591                                                cx.write_to_clipboard(ClipboardItem::new_string(
 592                                                    commit_sha.to_string(),
 593                                                ));
 594                                            })
 595                                    }),
 596                            )
 597                            .child(
 598                                h_flex()
 599                                    .gap_1p5()
 600                                    .child(
 601                                        Label::new(date_string)
 602                                            .color(Color::Muted)
 603                                            .size(LabelSize::Small),
 604                                    )
 605                                    .child(
 606                                        Label::new("")
 607                                            .color(Color::Ignored)
 608                                            .size(LabelSize::Small),
 609                                    )
 610                                    .children(commit_diff_stat),
 611                            ),
 612                    )
 613                    .children(remote_info.map(|(provider_name, url)| {
 614                        let icon = match provider_name.as_str() {
 615                            "GitHub" => IconName::Github,
 616                            _ => IconName::Link,
 617                        };
 618
 619                        Button::new("view_on_provider", format!("View on {}", provider_name))
 620                            .icon(icon)
 621                            .icon_color(Color::Muted)
 622                            .icon_size(IconSize::Small)
 623                            .icon_position(IconPosition::Start)
 624                            .on_click(move |_, _, cx| cx.open_url(&url))
 625                    })),
 626            )
 627    }
 628
 629    fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 630        Self::stash_action(
 631            workspace,
 632            "Apply",
 633            window,
 634            cx,
 635            async move |repository, sha, stash, commit_view, workspace, cx| {
 636                let result = repository.update(cx, |repo, cx| {
 637                    if !stash_matches_index(&sha, stash, repo) {
 638                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
 639                    }
 640                    Ok(repo.stash_apply(Some(stash), cx))
 641                });
 642
 643                match result {
 644                    Ok(task) => task.await?,
 645                    Err(err) => {
 646                        Self::close_commit_view(commit_view, workspace, cx).await?;
 647                        return Err(err);
 648                    }
 649                };
 650                Self::close_commit_view(commit_view, workspace, cx).await?;
 651                anyhow::Ok(())
 652            },
 653        );
 654    }
 655
 656    fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 657        Self::stash_action(
 658            workspace,
 659            "Pop",
 660            window,
 661            cx,
 662            async move |repository, sha, stash, commit_view, workspace, cx| {
 663                let result = repository.update(cx, |repo, cx| {
 664                    if !stash_matches_index(&sha, stash, repo) {
 665                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
 666                    }
 667                    Ok(repo.stash_pop(Some(stash), cx))
 668                });
 669
 670                match result {
 671                    Ok(task) => task.await?,
 672                    Err(err) => {
 673                        Self::close_commit_view(commit_view, workspace, cx).await?;
 674                        return Err(err);
 675                    }
 676                };
 677                Self::close_commit_view(commit_view, workspace, cx).await?;
 678                anyhow::Ok(())
 679            },
 680        );
 681    }
 682
 683    fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 684        Self::stash_action(
 685            workspace,
 686            "Drop",
 687            window,
 688            cx,
 689            async move |repository, sha, stash, commit_view, workspace, cx| {
 690                let result = repository.update(cx, |repo, cx| {
 691                    if !stash_matches_index(&sha, stash, repo) {
 692                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
 693                    }
 694                    Ok(repo.stash_drop(Some(stash), cx))
 695                });
 696
 697                match result {
 698                    Ok(task) => task.await??,
 699                    Err(err) => {
 700                        Self::close_commit_view(commit_view, workspace, cx).await?;
 701                        return Err(err);
 702                    }
 703                };
 704                Self::close_commit_view(commit_view, workspace, cx).await?;
 705                anyhow::Ok(())
 706            },
 707        );
 708    }
 709
 710    fn stash_action<AsyncFn>(
 711        workspace: &mut Workspace,
 712        str_action: &str,
 713        window: &mut Window,
 714        cx: &mut App,
 715        callback: AsyncFn,
 716    ) where
 717        AsyncFn: AsyncFnOnce(
 718                Entity<Repository>,
 719                &SharedString,
 720                usize,
 721                Entity<CommitView>,
 722                WeakEntity<Workspace>,
 723                &mut AsyncWindowContext,
 724            ) -> anyhow::Result<()>
 725            + 'static,
 726    {
 727        let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
 728            return;
 729        };
 730        let Some(stash) = commit_view.read(cx).stash else {
 731            return;
 732        };
 733        let sha = commit_view.read(cx).commit.sha.clone();
 734        let answer = window.prompt(
 735            PromptLevel::Info,
 736            &format!("{} stash@{{{}}}?", str_action, stash),
 737            None,
 738            &[str_action, "Cancel"],
 739            cx,
 740        );
 741
 742        let workspace_weak = workspace.weak_handle();
 743        let commit_view_entity = commit_view;
 744
 745        window
 746            .spawn(cx, async move |cx| {
 747                if answer.await != Ok(0) {
 748                    return anyhow::Ok(());
 749                }
 750
 751                let Some(workspace) = workspace_weak.upgrade() else {
 752                    return Ok(());
 753                };
 754
 755                let repo = workspace.update(cx, |workspace, cx| {
 756                    workspace
 757                        .panel::<GitPanel>(cx)
 758                        .and_then(|p| p.read(cx).active_repository.clone())
 759                });
 760
 761                let Some(repo) = repo else {
 762                    return Ok(());
 763                };
 764
 765                callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
 766                anyhow::Ok(())
 767            })
 768            .detach_and_notify_err(window, cx);
 769    }
 770
 771    async fn close_commit_view(
 772        commit_view: Entity<CommitView>,
 773        workspace: WeakEntity<Workspace>,
 774        cx: &mut AsyncWindowContext,
 775    ) -> anyhow::Result<()> {
 776        workspace
 777            .update_in(cx, |workspace, window, cx| {
 778                let active_pane = workspace.active_pane();
 779                let commit_view_id = commit_view.entity_id();
 780                active_pane.update(cx, |pane, cx| {
 781                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
 782                })
 783            })?
 784            .await?;
 785        anyhow::Ok(())
 786    }
 787}
 788
 789impl language::File for GitBlob {
 790    fn as_local(&self) -> Option<&dyn language::LocalFile> {
 791        None
 792    }
 793
 794    fn disk_state(&self) -> DiskState {
 795        DiskState::Historic {
 796            was_deleted: self.is_deleted,
 797        }
 798    }
 799
 800    fn path_style(&self, _: &App) -> PathStyle {
 801        PathStyle::local()
 802    }
 803
 804    fn path(&self) -> &Arc<RelPath> {
 805        self.path.as_ref()
 806    }
 807
 808    fn full_path(&self, _: &App) -> PathBuf {
 809        self.path.as_std_path().to_path_buf()
 810    }
 811
 812    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
 813        self.display_name.as_ref()
 814    }
 815
 816    fn worktree_id(&self, _: &App) -> WorktreeId {
 817        self.worktree_id
 818    }
 819
 820    fn to_proto(&self, _cx: &App) -> language::proto::File {
 821        unimplemented!()
 822    }
 823
 824    fn is_private(&self) -> bool {
 825        false
 826    }
 827
 828    fn can_open(&self) -> bool {
 829        !self.is_binary
 830    }
 831}
 832
 833async fn build_buffer(
 834    mut text: String,
 835    blob: Arc<dyn File>,
 836    language_registry: &Arc<language::LanguageRegistry>,
 837    cx: &mut AsyncApp,
 838) -> Result<Entity<Buffer>> {
 839    let line_ending = LineEnding::detect(&text);
 840    LineEnding::normalize(&mut text);
 841    let text = Rope::from(text);
 842    let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx));
 843    let language = if let Some(language) = language {
 844        language_registry
 845            .load_language(&language)
 846            .await
 847            .ok()
 848            .and_then(|e| e.log_err())
 849    } else {
 850        None
 851    };
 852    let buffer = cx.new(|cx| {
 853        let buffer = TextBuffer::new_normalized(
 854            ReplicaId::LOCAL,
 855            cx.entity_id().as_non_zero_u64().into(),
 856            line_ending,
 857            text,
 858        );
 859        let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
 860        buffer.set_language_async(language, cx);
 861        buffer
 862    });
 863    Ok(buffer)
 864}
 865
 866async fn build_buffer_diff(
 867    mut old_text: Option<String>,
 868    buffer: &Entity<Buffer>,
 869    language_registry: &Arc<LanguageRegistry>,
 870    cx: &mut AsyncApp,
 871) -> Result<Entity<BufferDiff>> {
 872    if let Some(old_text) = &mut old_text {
 873        LineEnding::normalize(old_text);
 874    }
 875
 876    let language = cx.update(|cx| buffer.read(cx).language().cloned());
 877    let buffer = cx.update(|cx| buffer.read(cx).snapshot());
 878
 879    let diff = cx.new(|cx| BufferDiff::new(&buffer.text, cx));
 880
 881    let update = diff
 882        .update(cx, |diff, cx| {
 883            diff.update_diff(
 884                buffer.text.clone(),
 885                old_text.map(|old_text| Arc::from(old_text.as_str())),
 886                true,
 887                language.clone(),
 888                cx,
 889            )
 890        })
 891        .await;
 892
 893    diff.update(cx, |diff, cx| {
 894        diff.language_changed(language, Some(language_registry.clone()), cx);
 895        diff.set_snapshot(update, &buffer.text, cx)
 896    })
 897    .await;
 898
 899    Ok(diff)
 900}
 901
 902impl EventEmitter<EditorEvent> for CommitView {}
 903
 904impl Focusable for CommitView {
 905    fn focus_handle(&self, cx: &App) -> FocusHandle {
 906        self.editor.focus_handle(cx)
 907    }
 908}
 909
 910impl Item for CommitView {
 911    type Event = EditorEvent;
 912
 913    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 914        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
 915    }
 916
 917    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 918        Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
 919            .color(if params.selected {
 920                Color::Default
 921            } else {
 922                Color::Muted
 923            })
 924            .into_any_element()
 925    }
 926
 927    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 928        let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
 929        let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
 930        format!("{short_sha}{subject}").into()
 931    }
 932
 933    fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
 934        let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
 935        let subject = self.commit.message.split('\n').next().unwrap();
 936
 937        Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
 938            let subject = subject.to_string();
 939            let short_sha = short_sha.to_string();
 940
 941            move |_, _| {
 942                v_flex()
 943                    .child(Label::new(subject.clone()))
 944                    .child(
 945                        Label::new(short_sha.clone())
 946                            .color(Color::Muted)
 947                            .size(LabelSize::Small),
 948                    )
 949                    .into_any_element()
 950            }
 951        }))))
 952    }
 953
 954    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 955        Editor::to_item_events(event, f)
 956    }
 957
 958    fn telemetry_event_text(&self) -> Option<&'static str> {
 959        Some("Commit View Opened")
 960    }
 961
 962    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 963        self.editor
 964            .update(cx, |editor, cx| editor.deactivated(window, cx));
 965    }
 966
 967    fn act_as_type<'a>(
 968        &'a self,
 969        type_id: TypeId,
 970        self_handle: &'a Entity<Self>,
 971        _: &'a App,
 972    ) -> Option<gpui::AnyEntity> {
 973        if type_id == TypeId::of::<Self>() {
 974            Some(self_handle.clone().into())
 975        } else if type_id == TypeId::of::<Editor>() {
 976            Some(self.editor.clone().into())
 977        } else {
 978            None
 979        }
 980    }
 981
 982    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
 983        Some(Box::new(self.editor.clone()))
 984    }
 985
 986    fn for_each_project_item(
 987        &self,
 988        cx: &App,
 989        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 990    ) {
 991        self.editor.for_each_project_item(cx, f)
 992    }
 993
 994    fn set_nav_history(
 995        &mut self,
 996        nav_history: ItemNavHistory,
 997        _: &mut Window,
 998        cx: &mut Context<Self>,
 999    ) {
1000        self.editor.update(cx, |editor, _| {
1001            editor.set_nav_history(Some(nav_history));
1002        });
1003    }
1004
1005    fn navigate(
1006        &mut self,
1007        data: Box<dyn Any>,
1008        window: &mut Window,
1009        cx: &mut Context<Self>,
1010    ) -> bool {
1011        self.editor
1012            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1013    }
1014
1015    fn added_to_workspace(
1016        &mut self,
1017        workspace: &mut Workspace,
1018        window: &mut Window,
1019        cx: &mut Context<Self>,
1020    ) {
1021        self.editor.update(cx, |editor, cx| {
1022            editor.added_to_workspace(workspace, window, cx)
1023        });
1024    }
1025
1026    fn can_split(&self) -> bool {
1027        true
1028    }
1029
1030    fn clone_on_split(
1031        &self,
1032        _workspace_id: Option<workspace::WorkspaceId>,
1033        window: &mut Window,
1034        cx: &mut Context<Self>,
1035    ) -> Task<Option<Entity<Self>>>
1036    where
1037        Self: Sized,
1038    {
1039        let file_statuses = self
1040            .editor
1041            .read(cx)
1042            .addon::<CommitDiffAddon>()
1043            .map(|addon| addon.file_statuses.clone())
1044            .unwrap_or_default();
1045        Task::ready(Some(cx.new(|cx| {
1046            let editor = cx.new({
1047                let file_statuses = file_statuses.clone();
1048                |cx| {
1049                    let mut editor = self
1050                        .editor
1051                        .update(cx, |editor, cx| editor.clone(window, cx));
1052                    editor.register_addon(CommitDiffAddon { file_statuses });
1053                    editor
1054                }
1055            });
1056            let multibuffer = editor.read(cx).buffer().clone();
1057            Self {
1058                editor,
1059                multibuffer,
1060                commit: self.commit.clone(),
1061                stash: self.stash,
1062                repository: self.repository.clone(),
1063                remote: self.remote.clone(),
1064            }
1065        })))
1066    }
1067}
1068
1069impl Render for CommitView {
1070    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1071        let is_stash = self.stash.is_some();
1072
1073        v_flex()
1074            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
1075            .size_full()
1076            .bg(cx.theme().colors().editor_background)
1077            .child(self.render_header(window, cx))
1078            .when(!self.editor.read(cx).is_empty(cx), |this| {
1079                this.child(div().flex_grow().child(self.editor.clone()))
1080            })
1081    }
1082}
1083
1084pub struct CommitViewToolbar {
1085    commit_view: Option<WeakEntity<CommitView>>,
1086}
1087
1088impl CommitViewToolbar {
1089    pub fn new() -> Self {
1090        Self { commit_view: None }
1091    }
1092}
1093
1094impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
1095
1096impl Render for CommitViewToolbar {
1097    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1098        div().hidden()
1099    }
1100}
1101
1102impl ToolbarItemView for CommitViewToolbar {
1103    fn set_active_pane_item(
1104        &mut self,
1105        active_pane_item: Option<&dyn ItemHandle>,
1106        _: &mut Window,
1107        cx: &mut Context<Self>,
1108    ) -> ToolbarItemLocation {
1109        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
1110            && entity.read(cx).stash.is_some()
1111        {
1112            self.commit_view = Some(entity.downgrade());
1113            return ToolbarItemLocation::PrimaryRight;
1114        }
1115        ToolbarItemLocation::Hidden
1116    }
1117
1118    fn pane_focus_update(
1119        &mut self,
1120        _pane_focused: bool,
1121        _window: &mut Window,
1122        _cx: &mut Context<Self>,
1123    ) {
1124    }
1125}
1126
1127fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
1128    repo.stash_entries
1129        .entries
1130        .get(stash_index)
1131        .map(|entry| entry.oid.to_string() == sha)
1132        .unwrap_or(false)
1133}