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