commit_view.rs

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