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