commit_view.rs

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