|
| 1 | +use crate::commit_view::CommitView; |
| 2 | +use git::Oid; |
| 3 | +use gpui::{Action, ClipboardItem, Entity, FocusHandle, SharedString, WeakEntity, Window, actions}; |
| 4 | +use project::{GIT_COMMAND_TASK_TAG, git_store::Repository}; |
| 5 | + |
| 6 | +use task::{TaskContext, TaskVariables, VariableName}; |
| 7 | +use ui::{Color, ContextMenu, ContextMenuEntry, IconName, IconPosition, prelude::*}; |
| 8 | +use workspace::Workspace; |
| 9 | + |
| 10 | +actions!( |
| 11 | + git_graph, |
| 12 | + [ |
| 13 | + /// Copies the SHA of the selected commit to the clipboard. |
| 14 | + CopyCommitSha, |
| 15 | + /// Copies a tag from the selected commit to the clipboard. |
| 16 | + CopyCommitTag, |
| 17 | + /// Opens the commit view for the selected commit. |
| 18 | + OpenCommitView, |
| 19 | + ] |
| 20 | +); |
| 21 | + |
| 22 | +const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.); |
| 23 | +const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands"; |
| 24 | + |
| 25 | +pub(crate) struct CommitContextMenuData { |
| 26 | + pub(crate) sha: Oid, |
| 27 | + pub(crate) tag_names: Vec<SharedString>, |
| 28 | +} |
| 29 | + |
| 30 | +#[derive(Clone, Copy, PartialEq, Eq)] |
| 31 | +pub(crate) enum CommitContextMenuSource { |
| 32 | + GitGraph, |
| 33 | + GitPanel, |
| 34 | +} |
| 35 | + |
| 36 | +pub(crate) fn commit_context_menu( |
| 37 | + commit: CommitContextMenuData, |
| 38 | + source: CommitContextMenuSource, |
| 39 | + ref_name: Option<SharedString>, |
| 40 | + focus_handle: FocusHandle, |
| 41 | + repository: Option<WeakEntity<Repository>>, |
| 42 | + workspace: WeakEntity<Workspace>, |
| 43 | + window: &mut Window, |
| 44 | + cx: &mut App, |
| 45 | +) -> Entity<ContextMenu> { |
| 46 | + let sha = commit.sha; |
| 47 | + let sha_short = sha.display_short(); |
| 48 | + let git_tasks = git_context_menu_tasks( |
| 49 | + git_task_context(&repository, sha, ref_name.as_deref(), cx), |
| 50 | + &workspace, |
| 51 | + cx, |
| 52 | + ); |
| 53 | + let header = match &ref_name { |
| 54 | + Some(ref_name) => format!("Ref {ref_name}"), |
| 55 | + None => format!("Commit {sha_short}"), |
| 56 | + }; |
| 57 | + |
| 58 | + ContextMenu::build(window, cx, move |context_menu, _, _| { |
| 59 | + context_menu |
| 60 | + .context(focus_handle) |
| 61 | + .header(header) |
| 62 | + .entry("View Commit", Some(OpenCommitView.boxed_clone()), { |
| 63 | + let repository = repository.clone(); |
| 64 | + let workspace = workspace.clone(); |
| 65 | + move |window, cx| { |
| 66 | + let Some(repository) = repository.clone() else { |
| 67 | + return; |
| 68 | + }; |
| 69 | + CommitView::open( |
| 70 | + sha.to_string(), |
| 71 | + repository, |
| 72 | + workspace.clone(), |
| 73 | + None, |
| 74 | + None, |
| 75 | + window, |
| 76 | + cx, |
| 77 | + ); |
| 78 | + } |
| 79 | + }) |
| 80 | + .entry( |
| 81 | + "Copy SHA", |
| 82 | + Some(CopyCommitSha.boxed_clone()), |
| 83 | + move |_window, cx| { |
| 84 | + cx.write_to_clipboard(ClipboardItem::new_string(sha.to_string())); |
| 85 | + }, |
| 86 | + ) |
| 87 | + .when_some(ref_name.clone(), |menu, ref_name| { |
| 88 | + menu.entry("Copy Ref Name", None, move |_window, cx| { |
| 89 | + cx.write_to_clipboard(ClipboardItem::new_string(ref_name.to_string())); |
| 90 | + }) |
| 91 | + }) |
| 92 | + .when(ref_name.is_none(), |menu| { |
| 93 | + menu.map(|menu| { |
| 94 | + let tag_names = commit.tag_names.clone(); |
| 95 | + let copy_tag_label = "Copy Tag"; |
| 96 | + |
| 97 | + match tag_names.as_slice() { |
| 98 | + [] => menu.item( |
| 99 | + ContextMenuEntry::new(copy_tag_label) |
| 100 | + .action(CopyCommitTag.boxed_clone()) |
| 101 | + .disabled(true), |
| 102 | + ), |
| 103 | + [tag_name] => { |
| 104 | + let tag_name = tag_name.clone(); |
| 105 | + let label = format!("{copy_tag_label}: {tag_name}"); |
| 106 | + menu.entry( |
| 107 | + label, |
| 108 | + Some(CopyCommitTag.boxed_clone()), |
| 109 | + move |_window, cx| { |
| 110 | + cx.write_to_clipboard(ClipboardItem::new_string( |
| 111 | + tag_name.to_string(), |
| 112 | + )); |
| 113 | + }, |
| 114 | + ) |
| 115 | + } |
| 116 | + _ => menu.submenu(copy_tag_label, move |menu, _window, _cx| { |
| 117 | + let mut menu = menu.fixed_width(COMMIT_TAG_LIST_WIDTH_IN_REMS.into()); |
| 118 | + |
| 119 | + for tag_name in tag_names.clone() { |
| 120 | + let tag_name_to_copy = tag_name.clone(); |
| 121 | + menu = menu.entry(tag_name, None, move |_window, cx| { |
| 122 | + cx.write_to_clipboard(ClipboardItem::new_string( |
| 123 | + tag_name_to_copy.to_string(), |
| 124 | + )); |
| 125 | + }); |
| 126 | + } |
| 127 | + menu |
| 128 | + }), |
| 129 | + } |
| 130 | + }) |
| 131 | + }) |
| 132 | + .when(source == CommitContextMenuSource::GitPanel, |menu| { |
| 133 | + menu.entry("Show in Git Graph", None, move |window, cx| { |
| 134 | + window.dispatch_action( |
| 135 | + Box::new(crate::git_graph::OpenAtCommit { |
| 136 | + sha: sha.to_string(), |
| 137 | + }), |
| 138 | + cx, |
| 139 | + ); |
| 140 | + }) |
| 141 | + }) |
| 142 | + .map(|mut menu| { |
| 143 | + menu = menu.separator().header("Custom Commands"); |
| 144 | + |
| 145 | + if git_tasks.is_empty() { |
| 146 | + return menu.item( |
| 147 | + ContextMenuEntry::new("Learn More") |
| 148 | + .icon(IconName::ArrowUpRight) |
| 149 | + .icon_color(Color::Muted) |
| 150 | + .icon_position(IconPosition::End) |
| 151 | + .handler(|_window, cx| { |
| 152 | + let docs_url = |
| 153 | + release_channel::docs_url(CUSTOM_GIT_COMMANDS_DOCS_SLUG, cx); |
| 154 | + cx.open_url(&docs_url); |
| 155 | + }), |
| 156 | + ); |
| 157 | + } |
| 158 | + |
| 159 | + for (task_source_kind, resolved_task) in git_tasks { |
| 160 | + let label = resolved_task.display_label().to_string(); |
| 161 | + let workspace = workspace.clone(); |
| 162 | + menu = menu.entry(label, None, move |window, cx| { |
| 163 | + workspace |
| 164 | + .update(cx, |workspace, cx| { |
| 165 | + workspace.schedule_resolved_task( |
| 166 | + task_source_kind.clone(), |
| 167 | + resolved_task.clone(), |
| 168 | + false, |
| 169 | + window, |
| 170 | + cx, |
| 171 | + ); |
| 172 | + }) |
| 173 | + .ok(); |
| 174 | + }); |
| 175 | + } |
| 176 | + |
| 177 | + menu |
| 178 | + }) |
| 179 | + }) |
| 180 | +} |
| 181 | + |
| 182 | +fn git_task_context( |
| 183 | + repository: &Option<WeakEntity<Repository>>, |
| 184 | + commit_sha: git::Oid, |
| 185 | + ref_name: Option<&str>, |
| 186 | + cx: &App, |
| 187 | +) -> Option<TaskContext> { |
| 188 | + let repository_path = repository |
| 189 | + .as_ref()? |
| 190 | + .upgrade()? |
| 191 | + .read(cx) |
| 192 | + .work_directory_abs_path |
| 193 | + .to_path_buf(); |
| 194 | + let repository_name = repository_path |
| 195 | + .file_name() |
| 196 | + .and_then(|name| name.to_str()) |
| 197 | + .map(ToString::to_string); |
| 198 | + let mut task_variables = TaskVariables::from_iter([ |
| 199 | + (VariableName::GitSha, commit_sha.to_string()), |
| 200 | + (VariableName::GitShaShort, commit_sha.display_short()), |
| 201 | + ( |
| 202 | + VariableName::GitRepositoryPath, |
| 203 | + repository_path.to_string_lossy().into_owned(), |
| 204 | + ), |
| 205 | + ]); |
| 206 | + |
| 207 | + if let Some(repository_name) = repository_name { |
| 208 | + task_variables.insert(VariableName::GitRepositoryName, repository_name); |
| 209 | + } |
| 210 | + if let Some(ref_name) = ref_name { |
| 211 | + task_variables.insert(VariableName::GitRef, ref_name.to_string()); |
| 212 | + } |
| 213 | + |
| 214 | + Some(TaskContext { |
| 215 | + cwd: Some(repository_path), |
| 216 | + task_variables, |
| 217 | + ..TaskContext::default() |
| 218 | + }) |
| 219 | +} |
| 220 | + |
| 221 | +fn git_context_menu_tasks( |
| 222 | + task_context: Option<TaskContext>, |
| 223 | + workspace: &WeakEntity<Workspace>, |
| 224 | + cx: &App, |
| 225 | +) -> Vec<(project::TaskSourceKind, task::ResolvedTask)> { |
| 226 | + let Some(task_context) = task_context else { |
| 227 | + return Vec::new(); |
| 228 | + }; |
| 229 | + let Some(workspace) = workspace.upgrade() else { |
| 230 | + return Vec::new(); |
| 231 | + }; |
| 232 | + let project = workspace.read(cx).project().clone(); |
| 233 | + let task_inventory = project.read_with(cx, |project, cx| { |
| 234 | + project.task_store().read(cx).task_inventory().cloned() |
| 235 | + }); |
| 236 | + let Some(task_inventory) = task_inventory else { |
| 237 | + return Vec::new(); |
| 238 | + }; |
| 239 | + |
| 240 | + task_inventory |
| 241 | + .read(cx) |
| 242 | + .resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, &task_context) |
| 243 | +} |
0 commit comments