commit_view.rs

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