Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:04.908Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

commit_view.rs

1381 lines · 49.8 KB · rust
1use anyhow::{Context as _, Result};
2use buffer_diff::BufferDiff;
3use collections::HashMap;
4use editor::{
5    Addon, Editor, EditorEvent, EditorSettings, MultiBuffer, RestoreOnlyDiffHunkDelegate,
6    SplittableEditor, hover_markdown_style, multibuffer_context_lines,
7};
8use futures_lite::future::yield_now;
9use git::repository::{CommitDetails, CommitDiff, RepoPath, is_binary_content};
10use git::status::{FileStatus, StatusCode, TrackedStatus};
11use git::{
12    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, ParsedGitRemote,
13    parse_git_remote_url,
14};
15use gpui::{
16    AnyElement, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, Entity,
17    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
18    PromptLevel, Render, ScrollHandle, StatefulInteractiveElement as _, Styled, Task, WeakEntity,
19    Window, actions,
20};
21use language::{
22    Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
23    ReplicaId, Rope, TextBuffer,
24};
25use markdown::{Markdown, MarkdownElement};
26use multi_buffer::PathKey;
27use project::{Project, ProjectPath, WorktreeId, git_store::Repository};
28use settings::{DiffViewStyle, Settings};
29use std::{
30    any::{Any, TypeId},
31    collections::HashSet,
32    path::PathBuf,
33    sync::Arc,
34};
35use theme::ActiveTheme;
36use ui::{ContextMenu, DiffStat, Disclosure, Divider, Tooltip, WithScrollbar, prelude::*};
37use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
38use workspace::item::TabTooltipContent;
39use workspace::{
40    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
41    Workspace,
42    item::{ItemEvent, TabContentParams},
43    notifications::NotifyTaskExt,
44    pane::SaveIntent,
45    searchable::SearchableItemHandle,
46};
47
48use crate::commit_tooltip::CommitAvatar;
49use crate::git_panel::GitPanel;
50
51actions!(
52    git,
53    [
54        ApplyCurrentStash,
55        PopCurrentStash,
56        DropCurrentStash,
57        OpenFileAtHead,
58    ]
59);
60
61pub fn init(cx: &mut App) {
62    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
63        workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {
64            CommitView::apply_stash(workspace, window, cx);
65        });
66        workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {
67            CommitView::remove_stash(workspace, window, cx);
68        });
69        workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {
70            CommitView::pop_stash(workspace, window, cx);
71        });
72    })
73    .detach();
74}
75
76pub struct CommitView {
77    commit: CommitDetails,
78    editor: Entity<SplittableEditor>,
79    message: Entity<Markdown>,
80    message_expanded: bool,
81    message_scroll_handle: ScrollHandle,
82    stash: Option<usize>,
83    multibuffer: Entity<MultiBuffer>,
84    repository: Entity<Repository>,
85    project: Entity<Project>,
86    workspace: WeakEntity<Workspace>,
87    remote: Option<GitRemote>,
88}
89
90struct GitBlob {
91    path: RepoPath,
92    worktree_id: WorktreeId,
93    is_deleted: bool,
94    is_binary: bool,
95    display_name: String,
96}
97
98struct CommitDiffAddon {
99    file_statuses: HashMap<language::BufferId, FileStatus>,
100    commit_view: WeakEntity<CommitView>,
101}
102
103impl Addon for CommitDiffAddon {
104    fn to_any(&self) -> &dyn std::any::Any {
105        self
106    }
107
108    fn override_status_for_buffer_id(
109        &self,
110        buffer_id: language::BufferId,
111        _cx: &App,
112    ) -> Option<FileStatus> {
113        self.file_statuses.get(&buffer_id).copied()
114    }
115
116    fn extend_buffer_header_context_menu(
117        &self,
118        menu: ContextMenu,
119        buffer: &language::BufferSnapshot,
120        _window: &mut Window,
121        cx: &mut App,
122    ) -> ContextMenu {
123        let file_to_open = buffer.file().and_then(|file| {
124            let commit_view = self.commit_view.upgrade()?;
125            let commit_view = commit_view.read(cx);
126            let project_path = commit_view
127                .repository
128                .read(cx)
129                .repo_path_to_project_path(&RepoPath::from_rel_path(file.path()), cx)?;
130            let exists_at_head = commit_view
131                .workspace
132                .upgrade()?
133                .read(cx)
134                .project()
135                .read(cx)
136                .entry_for_path(&project_path, cx)
137                .is_some();
138            exists_at_head.then(|| file.clone())
139        });
140
141        menu.when_some(file_to_open, |menu, file| {
142            let commit_view = self.commit_view.clone();
143            menu.entry(
144                "Open File in Project",
145                Some(Box::new(OpenFileAtHead)),
146                move |window, cx| {
147                    commit_view
148                        .update(cx, |view, cx| view.open_file_at_head(&file, window, cx))
149                        .log_err();
150                },
151            )
152        })
153    }
154}
155
156const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
157
158impl CommitView {
159    pub fn open(
160        commit_sha: String,
161        repo: WeakEntity<Repository>,
162        workspace: WeakEntity<Workspace>,
163        stash: Option<usize>,
164        file_filter: Option<RepoPath>,
165        window: &mut Window,
166        cx: &mut App,
167    ) {
168        let commit_diff = repo
169            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
170            .ok();
171        let commit_details = repo
172            .update(cx, |repo, _| repo.show(commit_sha.clone()))
173            .ok();
174
175        window
176            .spawn(cx, async move |cx| {
177                let commit_diff = commit_diff?;
178                let commit_details = commit_details?;
179                let (commit_diff, commit_details) = futures::join!(commit_diff, commit_details);
180                let mut commit_diff = commit_diff.log_err()?.log_err()?;
181                let commit_details = commit_details.log_err()?.log_err()?;
182
183                // Filter to specific file if requested
184                if let Some(ref filter_path) = file_filter {
185                    commit_diff.files.retain(|f| &f.path == filter_path);
186                }
187
188                let repo = repo.upgrade()?;
189
190                workspace
191                    .update_in(cx, |workspace, window, cx| {
192                        let project = workspace.project();
193                        let workspace_entity = cx.entity();
194                        let workspace_handle = cx.weak_entity();
195                        let commit_view = cx.new(|cx| {
196                            CommitView::new(
197                                commit_details,
198                                commit_diff,
199                                repo,
200                                project.clone(),
201                                workspace_entity,
202                                workspace_handle,
203                                stash,
204                                window,
205                                cx,
206                            )
207                        });
208
209                        let pane = workspace.active_pane();
210                        pane.update(cx, |pane, cx| {
211                            let ix = pane.items().position(|item| {
212                                let commit_view = item.downcast::<CommitView>();
213                                commit_view
214                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
215                            });
216                            if let Some(ix) = ix {
217                                let existing = pane
218                                    .items()
219                                    .filter_map(|item| item.downcast::<CommitView>())
220                                    .find(|view| view.read(cx).commit.sha == commit_sha)
221                                    .unwrap();
222
223                                pane.remove_item(existing.item_id(), false, false, window, cx);
224                                pane.add_item(
225                                    Box::new(commit_view),
226                                    true,
227                                    true,
228                                    Some(ix),
229                                    window,
230                                    cx,
231                                );
232                            } else {
233                                pane.add_item(Box::new(commit_view), true, true, None, window, cx);
234                            }
235                        })
236                    })
237                    .log_err()
238            })
239            .detach();
240    }
241
242    fn new(
243        commit: CommitDetails,
244        commit_diff: CommitDiff,
245        repository: Entity<Repository>,
246        project: Entity<Project>,
247        workspace_entity: Entity<Workspace>,
248        workspace: WeakEntity<Workspace>,
249        stash: Option<usize>,
250        window: &mut Window,
251        cx: &mut Context<Self>,
252    ) -> Self {
253        let language_registry = project.read(cx).languages().clone();
254        let multibuffer = cx.new(|cx| {
255            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
256            multibuffer.set_all_diff_hunks_expanded(cx);
257            multibuffer
258        });
259
260        let message = cx.new(|cx| {
261            Markdown::new(
262                commit.message.clone(),
263                Some(language_registry.clone()),
264                None,
265                cx,
266            )
267        });
268
269        let editor = cx.new(|cx| {
270            let editor = SplittableEditor::new(
271                EditorSettings::get_global(cx).diff_view_style,
272                multibuffer.clone(),
273                project.clone(),
274                workspace_entity.clone(),
275                window,
276                cx,
277            );
278            editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);
279
280            editor.rhs_editor().update(cx, |editor, cx| {
281                editor.set_show_bookmarks(false, cx);
282                editor.set_show_breakpoints(false, cx);
283                editor.set_show_diff_review_button(true, cx);
284            });
285
286            editor
287        });
288        let commit_sha = Arc::<str>::from(commit.sha.as_ref());
289
290        let first_worktree_id = project
291            .read(cx)
292            .worktrees(cx)
293            .next()
294            .map(|worktree| worktree.read(cx).id());
295
296        let repository_clone = repository.clone();
297
298        cx.spawn_in(window, async move |this, cx| {
299            let mut binary_buffer_ids: HashSet<language::BufferId> = HashSet::default();
300            let mut file_statuses: HashMap<language::BufferId, FileStatus> = HashMap::default();
301
302            for file in commit_diff.files {
303                let is_created = file.old_text.is_none();
304                let is_deleted = file.new_text.is_none();
305                let raw_new_text = file.new_text.unwrap_or_default();
306                let raw_old_text = file.old_text;
307
308                let is_binary = file.is_binary
309                    || is_binary_content(raw_new_text.as_bytes())
310                    || raw_old_text
311                        .as_ref()
312                        .is_some_and(|text| is_binary_content(text.as_bytes()));
313
314                let new_text = if is_binary {
315                    "(binary file not shown)".to_string()
316                } else {
317                    raw_new_text
318                };
319                let old_text = if is_binary { None } else { raw_old_text };
320                let worktree_id = repository_clone
321                    .update(cx, |repository, cx| {
322                        repository
323                            .repo_path_to_project_path(&file.path, cx)
324                            .map(|path| path.worktree_id)
325                            .or(first_worktree_id)
326                    })
327                    .context("project has no worktrees")?;
328                let short_sha = commit_sha
329                    .get(0..git::SHORT_SHA_LENGTH)
330                    .unwrap_or(&commit_sha);
331                let file_name = file
332                    .path
333                    .file_name()
334                    .map(|name| name.to_string())
335                    .unwrap_or_else(|| file.path.display(PathStyle::local()).to_string());
336                let display_name = format!("{short_sha} - {file_name}");
337
338                let file = Arc::new(GitBlob {
339                    path: file.path.clone(),
340                    is_deleted,
341                    is_binary,
342                    worktree_id,
343                    display_name,
344                }) as Arc<dyn language::File>;
345
346                let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
347                let buffer_id = cx.update(|_, cx| buffer.read(cx).remote_id())?;
348
349                let status_code = if is_created {
350                    StatusCode::Added
351                } else if is_deleted {
352                    StatusCode::Deleted
353                } else {
354                    StatusCode::Modified
355                };
356                file_statuses.insert(
357                    buffer_id,
358                    FileStatus::Tracked(TrackedStatus {
359                        index_status: status_code,
360                        worktree_status: StatusCode::Unmodified,
361                    }),
362                );
363
364                if is_binary {
365                    binary_buffer_ids.insert(buffer_id);
366                }
367
368                let buffer_diff = if is_binary {
369                    cx.update(|_, cx| {
370                        let snapshot = buffer.read(cx).snapshot();
371                        cx.new(|cx| {
372                            BufferDiff::new_unchanged(
373                                &snapshot,
374                                snapshot.language().cloned(),
375                                Some(language_registry.clone()),
376                                cx,
377                            )
378                        })
379                    })?
380                } else {
381                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?
382                };
383
384                let (excerpt_ranges, path) = cx.update(|_, cx| {
385                    let snapshot = buffer.read(cx).snapshot();
386                    let path = PathKey::with_sort_prefix(
387                        FILE_NAMESPACE_SORT_PREFIX,
388                        snapshot.file().unwrap().path().clone(),
389                    );
390                    let ranges = if is_binary {
391                        vec![language::Point::zero()..snapshot.max_point()]
392                    } else {
393                        let diff_snapshot = buffer_diff.read(cx).snapshot(cx);
394                        let mut hunks = diff_snapshot.hunks(&snapshot).peekable();
395                        if hunks.peek().is_none() {
396                            vec![language::Point::zero()..snapshot.max_point()]
397                        } else {
398                            hunks
399                                .map(|hunk| hunk.buffer_range.to_point(&snapshot))
400                                .collect::<Vec<_>>()
401                        }
402                    };
403                    (ranges, path)
404                })?;
405
406                // Batch the insertion of excerpts and yield between batches, to avoid blocking the main thread when a single file has many hunks.
407                const EXCERPT_BATCH_SIZE: usize = 10;
408                let total = excerpt_ranges.len();
409                let mut batch_end = 0;
410                while batch_end < total {
411                    let is_first_batch = batch_end == 0;
412                    batch_end = (batch_end + EXCERPT_BATCH_SIZE).min(total);
413                    let ranges = excerpt_ranges[..batch_end].to_vec();
414                    this.update_in(cx, |this, window, cx| {
415                        this.editor.update(cx, |editor, cx| {
416                            editor.update_excerpts_for_path(
417                                path.clone(),
418                                buffer.clone(),
419                                ranges,
420                                multibuffer_context_lines(cx),
421                                buffer_diff.clone(),
422                                cx,
423                            );
424                            if is_first_batch && editor.diff_view_style() == DiffViewStyle::Split {
425                                editor.split(window, cx);
426                            }
427                        });
428                    })?;
429                    if batch_end < total {
430                        yield_now().await;
431                    }
432                }
433            }
434
435            this.update(cx, |this, cx| {
436                let commit_view = cx.weak_entity();
437                this.editor.update(cx, |editor, cx| {
438                    editor.rhs_editor().update(cx, |editor, _cx| {
439                        editor.register_addon(CommitDiffAddon {
440                            file_statuses,
441                            commit_view,
442                        });
443                    });
444                });
445                if !binary_buffer_ids.is_empty() {
446                    this.editor.update(cx, |editor, cx| {
447                        editor.rhs_editor().update(cx, |editor, cx| {
448                            editor.fold_buffers(binary_buffer_ids, cx);
449                        });
450                    });
451                }
452            })?;
453
454            anyhow::Ok(())
455        })
456        .detach();
457
458        let snapshot = repository.read(cx).snapshot();
459        let remote_url = snapshot
460            .remote_upstream_url
461            .as_ref()
462            .or(snapshot.remote_origin_url.as_ref());
463
464        let remote = remote_url.and_then(|url| {
465            let provider_registry = GitHostingProviderRegistry::default_global(cx);
466            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
467                host,
468                owner: parsed.owner.into(),
469                repo: parsed.repo.into(),
470            })
471        });
472
473        Self {
474            commit,
475            editor,
476            message,
477            message_expanded: false,
478            message_scroll_handle: ScrollHandle::new(),
479            multibuffer,
480            stash,
481            repository,
482            project,
483            workspace,
484            remote,
485        }
486    }
487
488    fn render_commit_avatar(
489        &self,
490        sha: &SharedString,
491        size: impl Into<gpui::AbsoluteLength>,
492        window: &mut Window,
493        cx: &mut App,
494    ) -> AnyElement {
495        CommitAvatar::new(
496            sha,
497            Some(self.commit.author_email.clone()),
498            self.remote.as_ref(),
499        )
500        .size(size)
501        .render(window, cx)
502    }
503
504    fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
505        self.multibuffer.read(cx).snapshot(cx).total_changed_lines()
506    }
507
508    fn open_file_at_head(
509        &mut self,
510        file: &Arc<dyn language::File>,
511        window: &mut Window,
512        cx: &mut Context<Self>,
513    ) {
514        let rel_path = file.path().clone();
515        let worktree_id = file.worktree_id(cx);
516        let repo_path = RepoPath::from_rel_path(&rel_path);
517        let project_path = self
518            .repository
519            .read(cx)
520            .repo_path_to_project_path(&repo_path, cx)
521            .unwrap_or(project::ProjectPath {
522                worktree_id,
523                path: rel_path,
524            });
525
526        self.workspace
527            .update(cx, |workspace, cx| {
528                workspace
529                    .open_path_preview(project_path, None, false, false, true, window, cx)
530                    .detach_and_log_err(cx);
531            })
532            .log_err();
533    }
534
535    fn open_file_at_head_action(
536        &mut self,
537        _: &OpenFileAtHead,
538        window: &mut Window,
539        cx: &mut Context<Self>,
540    ) {
541        let Some(file) = self
542            .editor
543            .read(cx)
544            .focused_editor()
545            .read(cx)
546            .active_buffer(cx)
547            .and_then(|buffer| buffer.read(cx).file().cloned())
548        else {
549            return;
550        };
551        self.open_file_at_head(&file, window, cx);
552    }
553
554    fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
555        let commit = &self.commit;
556        let author_name = commit.author_name.clone();
557        let author_email = commit.author_email.clone();
558        let commit_sha = commit.sha.clone();
559        let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
560            .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
561        let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
562        let date_string = time_format::format_localized_timestamp(
563            commit_date,
564            time::OffsetDateTime::now_utc(),
565            local_offset,
566            time_format::TimestampFormat::MediumAbsolute,
567        );
568
569        let avatar_size = rems_from_px(40.);
570        let avatar_size_px = avatar_size.to_pixels(window.rem_size());
571        let gutter_width = self.editor.update(cx, |editor, cx| {
572            let editor = editor.rhs_editor().clone();
573            editor.update(cx, |editor, cx| {
574                let snapshot = editor.snapshot(window, cx);
575                let style = editor.style(cx);
576                let font_id = window.text_system().resolve_font(&style.text.font());
577                let font_size = style.text.font_size.to_pixels(window.rem_size());
578                snapshot
579                    .gutter_dimensions(font_id, font_size, style, window, cx)
580                    .full_width()
581            })
582        });
583        let avatar_min_side_padding = rems_from_px(6.).to_pixels(window.rem_size());
584        let avatar_container_min = avatar_size_px + avatar_min_side_padding;
585        let avatar_container_width = gutter_width.max(avatar_container_min);
586
587        let clipboard_has_sha = cx
588            .read_from_clipboard()
589            .and_then(|entry| entry.text())
590            .map_or(false, |clipboard_text| {
591                clipboard_text.trim() == commit_sha.as_ref()
592            });
593
594        let (copy_icon, copy_icon_color) = if clipboard_has_sha {
595            (IconName::Check, Color::Success)
596        } else {
597            (IconName::Copy, Color::Muted)
598        };
599
600        let has_more = self.commit.message.trim().contains('\n');
601        let is_expanded = self.message_expanded;
602        let expand_tooltip = if is_expanded {
603            "Fold Commit Description"
604        } else {
605            "Expand Commit Description"
606        };
607
608        v_flex()
609            .w_full()
610            .py_2p5()
611            .gap_2()
612            .border_b_1()
613            .border_color(cx.theme().colors().border_variant)
614            .child(
615                h_flex()
616                    .pr_2p5()
617                    .w_full()
618                    .flex_wrap()
619                    .justify_between()
620                    .child(
621                        h_flex()
622                            .child(
623                                h_flex()
624                                    .flex_none()
625                                    .w(avatar_container_width)
626                                    .justify_center()
627                                    .child(self.render_commit_avatar(
628                                        &commit.sha,
629                                        avatar_size,
630                                        window,
631                                        cx,
632                                    )),
633                            )
634                            .child(
635                                v_flex()
636                                    .child(h_flex().gap_1().child(Label::new(author_name)).when(
637                                        has_more,
638                                        |this| {
639                                            this.child(
640                                                Disclosure::new(
641                                                    "commit-message-disclosure",
642                                                    is_expanded,
643                                                )
644                                                .closed_icon(IconName::ExpandVertical)
645                                                .opened_icon(IconName::FoldVertical)
646                                                .tooltip(Tooltip::text(expand_tooltip))
647                                                .on_click(cx.listener(|this, _, _, cx| {
648                                                    this.message_expanded = !this.message_expanded;
649                                                    cx.notify();
650                                                })),
651                                            )
652                                        },
653                                    ))
654                                    .child(
655                                        h_flex()
656                                            .gap_1p5()
657                                            .child(
658                                                Label::new(date_string)
659                                                    .color(Color::Muted)
660                                                    .size(LabelSize::Small),
661                                            )
662                                            .child(
663                                                Label::new("•")
664                                                    .size(LabelSize::Small)
665                                                    .color(Color::Muted)
666                                                    .alpha(0.5),
667                                            )
668                                            .child(
669                                                Label::new(author_email)
670                                                    .color(Color::Muted)
671                                                    .size(LabelSize::Small),
672                                            ),
673                                    ),
674                            ),
675                    )
676                    .when(self.stash.is_none(), |this| {
677                        this.child(
678                            Button::new("sha", "Commit SHA")
679                                .start_icon(
680                                    Icon::new(copy_icon)
681                                        .size(IconSize::Small)
682                                        .color(copy_icon_color),
683                                )
684                                .tooltip({
685                                    let commit_sha = commit_sha.clone();
686                                    move |_, cx| {
687                                        Tooltip::with_meta(
688                                            "Copy Commit SHA",
689                                            None,
690                                            commit_sha.clone(),
691                                            cx,
692                                        )
693                                    }
694                                })
695                                .on_click(move |_, _, cx| {
696                                    cx.stop_propagation();
697                                    cx.write_to_clipboard(ClipboardItem::new_string(
698                                        commit_sha.to_string(),
699                                    ));
700                                }),
701                        )
702                    }),
703            )
704            .children(self.render_commit_message(avatar_container_width, window, cx))
705    }
706
707    fn render_commit_message(
708        &self,
709        avatar_spacer: Pixels,
710        window: &mut Window,
711        cx: &mut Context<Self>,
712    ) -> Option<impl IntoElement> {
713        let message = self.commit.message.trim();
714        if message.is_empty() {
715            return None;
716        }
717
718        let markdown_style = hover_markdown_style(window, cx);
719
720        let is_expanded = self.message_expanded;
721
722        let has_more = message.contains('\n');
723        let collapsed = has_more && !is_expanded;
724        let collapsed_height = window.line_height();
725        let max_expanded_height = window.line_height() * 12.;
726
727        Some(
728            h_flex()
729                .w_full()
730                .pr_2p5()
731                .child(h_flex().flex_none().w(avatar_spacer))
732                .child(
733                    div()
734                        .relative()
735                        .flex_1()
736                        .min_w_0()
737                        .child(
738                            div()
739                                .id("commit-message")
740                                .size_full()
741                                .text_sm()
742                                .when(collapsed, |this| this.h(collapsed_height).overflow_hidden())
743                                .when(!collapsed, |this| {
744                                    this.max_h(max_expanded_height)
745                                        .overflow_y_scroll()
746                                        .track_scroll(&self.message_scroll_handle)
747                                })
748                                .child(MarkdownElement::new(self.message.clone(), markdown_style)),
749                        )
750                        .vertical_scrollbar_for(&self.message_scroll_handle, window, cx),
751                ),
752        )
753    }
754
755    fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
756        Self::stash_action(
757            workspace,
758            "Apply",
759            window,
760            cx,
761            async move |repository, sha, stash, commit_view, workspace, cx| {
762                let result = repository.update(cx, |repo, cx| {
763                    if !stash_matches_index(&sha, stash, repo) {
764                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
765                    }
766                    Ok(repo.stash_apply(Some(stash), cx))
767                });
768
769                match result {
770                    Ok(task) => task.await?,
771                    Err(err) => {
772                        Self::close_commit_view(commit_view, workspace, cx).await?;
773                        return Err(err);
774                    }
775                };
776                Self::close_commit_view(commit_view, workspace, cx).await?;
777                anyhow::Ok(())
778            },
779        );
780    }
781
782    fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
783        Self::stash_action(
784            workspace,
785            "Pop",
786            window,
787            cx,
788            async move |repository, sha, stash, commit_view, workspace, cx| {
789                let result = repository.update(cx, |repo, cx| {
790                    if !stash_matches_index(&sha, stash, repo) {
791                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
792                    }
793                    Ok(repo.stash_pop(Some(stash), cx))
794                });
795
796                match result {
797                    Ok(task) => task.await?,
798                    Err(err) => {
799                        Self::close_commit_view(commit_view, workspace, cx).await?;
800                        return Err(err);
801                    }
802                };
803                Self::close_commit_view(commit_view, workspace, cx).await?;
804                anyhow::Ok(())
805            },
806        );
807    }
808
809    fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
810        Self::stash_action(
811            workspace,
812            "Drop",
813            window,
814            cx,
815            async move |repository, sha, stash, commit_view, workspace, cx| {
816                let result = repository.update(cx, |repo, cx| {
817                    if !stash_matches_index(&sha, stash, repo) {
818                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
819                    }
820                    Ok(repo.stash_drop(Some(stash), cx))
821                });
822
823                match result {
824                    Ok(task) => task.await??,
825                    Err(err) => {
826                        Self::close_commit_view(commit_view, workspace, cx).await?;
827                        return Err(err);
828                    }
829                };
830                Self::close_commit_view(commit_view, workspace, cx).await?;
831                anyhow::Ok(())
832            },
833        );
834    }
835
836    fn stash_action<AsyncFn>(
837        workspace: &mut Workspace,
838        str_action: &str,
839        window: &mut Window,
840        cx: &mut App,
841        callback: AsyncFn,
842    ) where
843        AsyncFn: AsyncFnOnce(
844                Entity<Repository>,
845                &SharedString,
846                usize,
847                Entity<CommitView>,
848                WeakEntity<Workspace>,
849                &mut AsyncWindowContext,
850            ) -> anyhow::Result<()>
851            + 'static,
852    {
853        let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
854            return;
855        };
856        let Some(stash) = commit_view.read(cx).stash else {
857            return;
858        };
859        let sha = commit_view.read(cx).commit.sha.clone();
860        let answer = window.prompt(
861            PromptLevel::Info,
862            &format!("{} stash@{{{}}}?", str_action, stash),
863            None,
864            &[str_action, "Cancel"],
865            cx,
866        );
867
868        let workspace_weak = workspace.weak_handle();
869        let commit_view_entity = commit_view;
870
871        window
872            .spawn(cx, async move |cx| {
873                if answer.await != Ok(0) {
874                    return anyhow::Ok(());
875                }
876
877                let Some(workspace) = workspace_weak.upgrade() else {
878                    return Ok(());
879                };
880
881                let repo = workspace.update(cx, |workspace, cx| {
882                    workspace
883                        .panel::<GitPanel>(cx)
884                        .and_then(|p| p.read(cx).active_repository.clone())
885                });
886
887                let Some(repo) = repo else {
888                    return Ok(());
889                };
890
891                callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
892                anyhow::Ok(())
893            })
894            .detach_and_notify_err(workspace.weak_handle(), window, cx);
895    }
896
897    async fn close_commit_view(
898        commit_view: Entity<CommitView>,
899        workspace: WeakEntity<Workspace>,
900        cx: &mut AsyncWindowContext,
901    ) -> anyhow::Result<()> {
902        workspace
903            .update_in(cx, |workspace, window, cx| {
904                let active_pane = workspace.active_pane();
905                let commit_view_id = commit_view.entity_id();
906                active_pane.update(cx, |pane, cx| {
907                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
908                })
909            })?
910            .await?;
911        anyhow::Ok(())
912    }
913}
914
915impl language::File for GitBlob {
916    fn as_local(&self) -> Option<&dyn language::LocalFile> {
917        None
918    }
919
920    fn disk_state(&self) -> DiskState {
921        DiskState::Historic {
922            was_deleted: self.is_deleted,
923        }
924    }
925
926    fn path_style(&self, _: &App) -> PathStyle {
927        PathStyle::local()
928    }
929
930    fn path(&self) -> &Arc<RelPath> {
931        self.path.as_ref()
932    }
933
934    fn full_path(&self, _: &App) -> PathBuf {
935        self.path.as_std_path().to_path_buf()
936    }
937
938    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
939        self.display_name.as_ref()
940    }
941
942    fn worktree_id(&self, _: &App) -> WorktreeId {
943        self.worktree_id
944    }
945
946    fn to_proto(&self, _cx: &App) -> language::proto::File {
947        unimplemented!()
948    }
949
950    fn is_private(&self) -> bool {
951        false
952    }
953
954    fn can_open(&self) -> bool {
955        !self.is_binary
956    }
957}
958
959async fn build_buffer(
960    mut text: String,
961    blob: Arc<dyn File>,
962    language_registry: &Arc<language::LanguageRegistry>,
963    cx: &mut AsyncWindowContext,
964) -> Result<Entity<Buffer>> {
965    let line_ending = LineEnding::detect(&text);
966    LineEnding::normalize(&mut text);
967    let text = Rope::from(text);
968    let language =
969        cx.update(|_, cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
970    let language = if let Some(language_id) = language {
971        language_registry
972            .load_language(language_id)
973            .await
974            .ok()
975            .and_then(|e| e.log_err())
976    } else {
977        None
978    };
979    let buffer = cx.new(|cx| {
980        let buffer = TextBuffer::new_normalized(
981            ReplicaId::LOCAL,
982            cx.entity_id().as_non_zero_u64().into(),
983            line_ending,
984            text,
985        );
986        let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
987        buffer.set_language_async(language, cx);
988        buffer
989    });
990    Ok(buffer)
991}
992
993async fn build_buffer_diff(
994    mut old_text: Option<String>,
995    buffer: &Entity<Buffer>,
996    language_registry: &Arc<LanguageRegistry>,
997    cx: &mut AsyncWindowContext,
998) -> Result<Entity<BufferDiff>> {
999    if let Some(old_text) = &mut old_text {
1000        LineEnding::normalize(old_text);
1001    }
1002
1003    let language = cx.update(|_, cx| buffer.read(cx).language().cloned())?;
1004    let buffer = cx.update(|_, cx| buffer.read(cx).snapshot())?;
1005
1006    let diff =
1007        cx.new(|cx| BufferDiff::new(&buffer.text, language, Some(language_registry.clone()), cx));
1008
1009    diff.update(cx, |diff, cx| {
1010        diff.set_base_text(
1011            old_text.map(|old_text| Arc::from(old_text.as_str())),
1012            buffer.text.clone(),
1013            cx,
1014        )
1015    })
1016    .await;
1017
1018    Ok(diff)
1019}
1020
1021impl EventEmitter<EditorEvent> for CommitView {}
1022
1023impl Focusable for CommitView {
1024    fn focus_handle(&self, cx: &App) -> FocusHandle {
1025        self.editor.focus_handle(cx)
1026    }
1027}
1028
1029impl Item for CommitView {
1030    type Event = EditorEvent;
1031
1032    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
1033        Some(Icon::new(IconName::GitCommit).color(Color::Muted))
1034    }
1035
1036    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
1037        Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
1038            .color(if params.selected {
1039                Color::Default
1040            } else {
1041                Color::Muted
1042            })
1043            .into_any_element()
1044    }
1045
1046    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1047        let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
1048        let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
1049        format!("{short_sha} — {subject}").into()
1050    }
1051
1052    fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
1053        let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
1054        let subject = self.commit.message.split('\n').next().unwrap();
1055
1056        Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
1057            let subject = subject.to_string();
1058            let short_sha = short_sha.to_string();
1059
1060            move |_, _| {
1061                v_flex()
1062                    .child(Label::new(subject.clone()))
1063                    .child(
1064                        Label::new(short_sha.clone())
1065                            .color(Color::Muted)
1066                            .size(LabelSize::Small),
1067                    )
1068                    .into_any_element()
1069            }
1070        }))))
1071    }
1072
1073    fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
1074        Editor::to_item_events(event, f)
1075    }
1076
1077    fn telemetry_event_text(&self) -> Option<&'static str> {
1078        Some("Commit View Opened")
1079    }
1080
1081    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1082        self.editor
1083            .update(cx, |editor, cx| editor.deactivated(window, cx));
1084    }
1085
1086    fn act_as_type<'a>(
1087        &'a self,
1088        type_id: TypeId,
1089        self_handle: &'a Entity<Self>,
1090        cx: &'a App,
1091    ) -> Option<gpui::AnyEntity> {
1092        if type_id == TypeId::of::<Self>() {
1093            Some(self_handle.clone().into())
1094        } else if type_id == TypeId::of::<SplittableEditor>() {
1095            Some(self.editor.clone().into())
1096        } else if type_id == TypeId::of::<Editor>() {
1097            Some(self.editor.read(cx).rhs_editor().clone().into())
1098        } else {
1099            None
1100        }
1101    }
1102
1103    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
1104        Some(Box::new(self.editor.clone()))
1105    }
1106
1107    fn for_each_project_item(
1108        &self,
1109        cx: &App,
1110        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1111    ) {
1112        self.editor.read(cx).for_each_project_item(cx, f)
1113    }
1114
1115    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
1116        self.editor.read(cx).active_project_path(cx)
1117    }
1118
1119    fn set_nav_history(
1120        &mut self,
1121        nav_history: ItemNavHistory,
1122        _: &mut Window,
1123        cx: &mut Context<Self>,
1124    ) {
1125        self.editor.update(cx, |editor, cx| {
1126            editor.rhs_editor().update(cx, |editor, _| {
1127                editor.set_nav_history(Some(nav_history));
1128            });
1129        });
1130    }
1131
1132    fn navigate(
1133        &mut self,
1134        data: Arc<dyn Any + Send>,
1135        window: &mut Window,
1136        cx: &mut Context<Self>,
1137    ) -> bool {
1138        self.editor
1139            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1140    }
1141
1142    fn added_to_workspace(
1143        &mut self,
1144        workspace: &mut Workspace,
1145        window: &mut Window,
1146        cx: &mut Context<Self>,
1147    ) {
1148        self.editor.update(cx, |editor, cx| {
1149            editor.added_to_workspace(workspace, window, cx)
1150        });
1151    }
1152
1153    fn can_split(&self) -> bool {
1154        true
1155    }
1156
1157    fn clone_on_split(
1158        &self,
1159        _workspace_id: Option<workspace::WorkspaceId>,
1160        window: &mut Window,
1161        cx: &mut Context<Self>,
1162    ) -> Task<Option<Entity<Self>>>
1163    where
1164        Self: Sized,
1165    {
1166        let file_statuses = self
1167            .editor
1168            .read(cx)
1169            .rhs_editor()
1170            .read(cx)
1171            .addon::<CommitDiffAddon>()
1172            .map(|addon| addon.file_statuses.clone())
1173            .unwrap_or_default();
1174        let Some(workspace_entity) = self.workspace.upgrade() else {
1175            return Task::ready(None);
1176        };
1177        let project = self.project.clone();
1178        let diff_view_style = self.editor.read(cx).diff_view_style();
1179        let multibuffer = self.multibuffer.clone();
1180        Task::ready(Some(cx.new(|cx| {
1181            let commit_view = cx.weak_entity();
1182            let editor = cx.new({
1183                let file_statuses = file_statuses.clone();
1184                let project = project.clone();
1185                let workspace_entity = workspace_entity.clone();
1186                let multibuffer = multibuffer.clone();
1187                move |cx| {
1188                    let editor = SplittableEditor::new(
1189                        diff_view_style,
1190                        multibuffer.clone(),
1191                        project.clone(),
1192                        workspace_entity.clone(),
1193                        window,
1194                        cx,
1195                    );
1196                    editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);
1197                    editor.rhs_editor().update(cx, |editor, cx| {
1198                        editor.set_show_bookmarks(false, cx);
1199                        editor.set_show_breakpoints(false, cx);
1200                        editor.set_show_diff_review_button(true, cx);
1201                        editor.register_addon(CommitDiffAddon {
1202                            file_statuses,
1203                            commit_view,
1204                        });
1205                    });
1206                    editor
1207                }
1208            });
1209            let language_registry = project.read(cx).languages().clone();
1210            let message = cx.new(|cx| {
1211                Markdown::new(
1212                    self.commit.message.clone(),
1213                    Some(language_registry),
1214                    None,
1215                    cx,
1216                )
1217            });
1218            Self {
1219                editor,
1220                message,
1221                message_expanded: self.message_expanded,
1222                message_scroll_handle: ScrollHandle::new(),
1223                multibuffer: self.multibuffer.clone(),
1224                commit: self.commit.clone(),
1225                stash: self.stash,
1226                repository: self.repository.clone(),
1227                project: self.project.clone(),
1228                workspace: self.workspace.clone(),
1229                remote: self.remote.clone(),
1230            }
1231        })))
1232    }
1233}
1234
1235impl Render for CommitView {
1236    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1237        let is_stash = self.stash.is_some();
1238
1239        v_flex()
1240            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
1241            .on_action(cx.listener(Self::open_file_at_head_action))
1242            .size_full()
1243            .bg(cx.theme().colors().editor_background)
1244            .child(self.render_header(window, cx))
1245            .when(
1246                !self.editor.read(cx).rhs_editor().read(cx).is_empty(cx),
1247                |this| this.child(div().flex_grow(1.).child(self.editor.clone())),
1248            )
1249    }
1250}
1251
1252pub struct CommitViewToolbar {
1253    commit_view: Option<WeakEntity<CommitView>>,
1254}
1255
1256impl CommitViewToolbar {
1257    pub fn new() -> Self {
1258        Self { commit_view: None }
1259    }
1260}
1261
1262impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
1263
1264impl Render for CommitViewToolbar {
1265    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1266        let Some(commit_view) = self.commit_view.as_ref().and_then(|w| w.upgrade()) else {
1267            return div();
1268        };
1269
1270        let commit_view_ref = commit_view.read(cx);
1271        let is_stash = commit_view_ref.stash.is_some();
1272
1273        let (additions, deletions) = commit_view_ref.calculate_changed_lines(cx);
1274
1275        let commit_sha = commit_view_ref.commit.sha.clone();
1276
1277        let remote_info = commit_view_ref.remote.as_ref().map(|remote| {
1278            let provider = remote.host.name();
1279            let parsed_remote = ParsedGitRemote {
1280                owner: remote.owner.as_ref().into(),
1281                repo: remote.repo.as_ref().into(),
1282            };
1283            let params = BuildCommitPermalinkParams { sha: &commit_sha };
1284            let url = remote
1285                .host
1286                .build_commit_permalink(&parsed_remote, params)
1287                .to_string();
1288            (provider, url)
1289        });
1290
1291        let sha_for_graph = commit_sha.to_string();
1292
1293        h_flex()
1294            .gap_1()
1295            .when(additions > 0 || deletions > 0, |this| {
1296                this.child(
1297                    h_flex()
1298                        .gap_2()
1299                        .child(DiffStat::new(
1300                            "toolbar-diff-stat",
1301                            additions as usize,
1302                            deletions as usize,
1303                        ))
1304                        .child(Divider::vertical()),
1305                )
1306            })
1307            .child(
1308                IconButton::new("buffer-search", IconName::MagnifyingGlass)
1309                    .icon_size(IconSize::Small)
1310                    .tooltip(move |_, cx| {
1311                        Tooltip::for_action(
1312                            "Buffer Search",
1313                            &zed_actions::buffer_search::Deploy::find(),
1314                            cx,
1315                        )
1316                    })
1317                    .on_click(|_, window, cx| {
1318                        window.dispatch_action(
1319                            Box::new(zed_actions::buffer_search::Deploy::find()),
1320                            cx,
1321                        );
1322                    }),
1323            )
1324            .when(!is_stash, |this| {
1325                this.child(
1326                    IconButton::new("show-in-git-graph", IconName::GitGraph)
1327                        .icon_size(IconSize::Small)
1328                        .tooltip(Tooltip::text("Show in Git Graph"))
1329                        .on_click(move |_, window, cx| {
1330                            window.dispatch_action(
1331                                Box::new(crate::git_graph::OpenAtCommit {
1332                                    sha: sha_for_graph.clone(),
1333                                }),
1334                                cx,
1335                            );
1336                        }),
1337                )
1338                .children(remote_info.map(|(provider_name, url)| {
1339                    let icon = crate::get_provider_icon(provider_name.as_str());
1340
1341                    IconButton::new("view_on_provider", icon)
1342                        .icon_size(IconSize::Small)
1343                        .tooltip(Tooltip::text(format!("View on {}", provider_name)))
1344                        .on_click(move |_, _, cx| cx.open_url(&url))
1345                }))
1346            })
1347    }
1348}
1349
1350impl ToolbarItemView for CommitViewToolbar {
1351    fn set_active_pane_item(
1352        &mut self,
1353        active_pane_item: Option<&dyn ItemHandle>,
1354        _: &mut Window,
1355        cx: &mut Context<Self>,
1356    ) -> ToolbarItemLocation {
1357        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx)) {
1358            self.commit_view = Some(entity.downgrade());
1359            return ToolbarItemLocation::PrimaryRight;
1360        }
1361        self.commit_view = None;
1362        ToolbarItemLocation::Hidden
1363    }
1364
1365    fn pane_focus_update(
1366        &mut self,
1367        _pane_focused: bool,
1368        _window: &mut Window,
1369        _cx: &mut Context<Self>,
1370    ) {
1371    }
1372}
1373
1374fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
1375    repo.stash_entries
1376        .entries
1377        .get(stash_index)
1378        .map(|entry| entry.oid.to_string() == sha)
1379        .unwrap_or(false)
1380}
1381
Served at tenant.openagents/omega Member data and write actions are omitted.