Add support for custom git commands in the git graph context menu (#56354)
This PR adds support to create custom git task that are triggered from
the git graph context menu. Both Sublime Merge and Fork have custom
command support, and I use custom commands I've built out frequently to
speed up some of my tasks at Zed. Thus, I'd like to have support in
Zed's git graph so I can use Zed even more!
It offers initial support for the following env variables:
- `ZED_GIT_SHA`
- `ZED_GIT_SHA_SHORT`
- `ZED_GIT_REPOSITORY_NAME`
- `ZED_GIT_REPOSITORY_PATH`
These are only populated in the git graph context.
This PR also introduces a the `git-command` tag, which is needed so the
git graph can filter down to these custom git commands, and so other
tasks aren't polluting the context menu.
An example would be (in the global `tasks.json`):
```json
{
"label": "Branches containing commit: $ZED_GIT_SHA_SHORT",
"command": "git",
"args": ["branch", "-a", "--contains", "$ZED_GIT_SHA"],
"tags": ["git-command"],
},
```
And then in the context menu:
<img width="646" height="296" alt="SCR-20260511-mnfa"
src="https://github.com/user-attachments/assets/0e7b811b-f47d-4a2f-9270-99e392c38663"
/>
And the output in the terminal:
<img width="585" height="184" alt="SCR-20260511-mnks"
src="https://github.com/user-attachments/assets/54d7d205-6212-4eff-8dbb-c8e908996747"
/>
The awesome thing about using tasks is we get all the task
infrastructure for free, such as history!
<img width="591" height="292" alt="SCR-20260511-mnud"
src="https://github.com/user-attachments/assets/6315be8f-dd33-470f-bfcd-aa56d7fbfdce"
/>
<img width="602" height="173" alt="SCR-20260511-moch"
src="https://github.com/user-attachments/assets/b528422c-efcc-4a7d-9783-73d945e9665b"
/>
And we have all the task configuration options too out of the box.
---
Right now, this only works with global tasks. It isn't clear how to shoe
in support for worktree-specific tasks (`.zed/tasks.json`) in a clear
way, as not all invocations of the git graph are in an area that has a
clear worktree id, and so we sort of have to guess or fallback to
something else. That can be a followup once it's more clear how we
should cover that. Or maybe someone else has a better solution.
I chose to only ship with these 4 git-specific variables for now. The
git graph also currently ONLY resolve those variables. We can adjust /
add in more in follow up PRs.
Self-Review Checklist:
- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable
Release Notes:
- Added support for running global custom Git command tasks from the Git
Graph commit context menu.
This commit is contained in:
1 parent
26bdcd451b
commit
56fc262fdb
8 files changed
+386
-7
No files matched your search
Generated
+1
@@ -7351,6 +7351,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"task",
|
||||
"theme",
|
||||
"theme_settings",
|
||||
"time",
|
||||
|
||||
@@ -36,6 +36,7 @@ project_panel.workspace = true
|
||||
search.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -22,7 +22,7 @@ use language::line_diff;
|
||||
use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
ProjectPath,
|
||||
GIT_COMMAND_TASK_TAG, ProjectPath, TaskSourceKind,
|
||||
git_store::{
|
||||
CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository,
|
||||
RepositoryEvent, RepositoryId,
|
||||
@@ -41,6 +41,7 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use task::{ResolvedTask, TaskContext, TaskVariables, VariableName};
|
||||
use theme::AccentColors;
|
||||
use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};
|
||||
use ui::{
|
||||
@@ -2031,6 +2032,82 @@ impl GitGraph {
|
||||
self.copy_commit_tag(selected_entry_index, window, cx);
|
||||
}
|
||||
|
||||
fn git_task_context(&self, commit_sha: Oid, cx: &App) -> Option<TaskContext> {
|
||||
let repository_path = self
|
||||
.get_repository(cx)?
|
||||
.read(cx)
|
||||
.work_directory_abs_path
|
||||
.to_path_buf();
|
||||
|
||||
let repository_name = repository_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(ToString::to_string);
|
||||
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::GitSha, commit_sha.to_string()),
|
||||
(VariableName::GitShaShort, commit_sha.display_short()),
|
||||
(
|
||||
VariableName::GitRepositoryPath,
|
||||
repository_path.to_string_lossy().into_owned(),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(repository_name) = repository_name {
|
||||
task_variables.insert(VariableName::GitRepositoryName, repository_name);
|
||||
}
|
||||
|
||||
Some(TaskContext {
|
||||
cwd: Some(repository_path),
|
||||
task_variables,
|
||||
..TaskContext::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn git_context_menu_tasks(
|
||||
&self,
|
||||
task_context: &TaskContext,
|
||||
cx: &App,
|
||||
) -> Vec<(TaskSourceKind, ResolvedTask)> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
let task_inventory = project.read_with(cx, |project, cx| {
|
||||
project.task_store().read(cx).task_inventory().cloned()
|
||||
});
|
||||
|
||||
let Some(task_inventory) = task_inventory else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
task_inventory
|
||||
.read(cx)
|
||||
.resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, task_context)
|
||||
}
|
||||
|
||||
fn schedule_git_task(
|
||||
&mut self,
|
||||
task_source_kind: TaskSourceKind,
|
||||
resolved_task: ResolvedTask,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.schedule_resolved_task(
|
||||
task_source_kind,
|
||||
resolved_task,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn deploy_entry_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
@@ -2041,7 +2118,8 @@ impl GitGraph {
|
||||
let Some(commit) = self.graph_data.commits.get(index) else {
|
||||
return;
|
||||
};
|
||||
let short_sha = commit.data.sha.display_short();
|
||||
let sha = commit.data.sha;
|
||||
let sha_short = sha.display_short();
|
||||
let tag_names = commit.data.tag_names();
|
||||
let copy_tag_label = "Copy Tag";
|
||||
let copy_tag_label: SharedString = match tag_names.as_slice() {
|
||||
@@ -2050,13 +2128,17 @@ impl GitGraph {
|
||||
_ => format!("{copy_tag_label}…").into(),
|
||||
};
|
||||
let copy_tag_disabled = tag_names.is_empty();
|
||||
let git_tasks = self
|
||||
.git_task_context(sha, cx)
|
||||
.map(|task_context| self.git_context_menu_tasks(&task_context, cx))
|
||||
.unwrap_or_default();
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let git_graph = cx.entity();
|
||||
let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.header(format!("Commit {short_sha}"))
|
||||
.header(format!("Commit {sha_short}"))
|
||||
.entry(
|
||||
"View Commit",
|
||||
Some(OpenCommitView.boxed_clone()),
|
||||
@@ -2079,6 +2161,28 @@ impl GitGraph {
|
||||
this.copy_commit_tag(index, window, cx);
|
||||
})),
|
||||
)
|
||||
.when(!git_tasks.is_empty(), |mut menu| {
|
||||
menu = menu.separator().header("Custom Git Commands");
|
||||
|
||||
for (task_source_kind, resolved_task) in git_tasks {
|
||||
let label = resolved_task.display_label().to_string();
|
||||
|
||||
menu = menu.entry(
|
||||
label,
|
||||
None,
|
||||
window.handler_for(&git_graph, move |this, window, cx| {
|
||||
this.schedule_git_task(
|
||||
task_source_kind.clone(),
|
||||
resolved_task.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
});
|
||||
self.set_context_menu(context_menu, position, index, window, cx);
|
||||
}
|
||||
@@ -3883,8 +3987,8 @@ mod tests {
|
||||
use git::Oid;
|
||||
use git::repository::InitialGraphCommitData;
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use project::Project;
|
||||
use project::git_store::{GitStoreEvent, RepositoryEvent};
|
||||
use project::{Project, TaskSourceKind, task_store::TaskSettingsLocation};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{SettingsStore, ThemeSettingsContent};
|
||||
@@ -5934,4 +6038,158 @@ mod tests {
|
||||
assert_eq!(graph.selected_entry_idx, Some(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_global_git_command_task_runs_from_context_menu(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
Path::new("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": "content",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let commit_sha = Oid::try_from("abcdef1234567890abcdef1234567890abcdef12")
|
||||
.expect("commit SHA should be valid");
|
||||
fs.set_graph_commits(
|
||||
Path::new("/project/.git"),
|
||||
vec![Arc::new(InitialGraphCommitData {
|
||||
sha: commit_sha,
|
||||
parents: SmallVec::new(),
|
||||
ref_names: Vec::new(),
|
||||
})],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let repository = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.active_repository(cx)
|
||||
.expect("project should have an active repository")
|
||||
});
|
||||
let task_inventory = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.task_store()
|
||||
.read(cx)
|
||||
.task_inventory()
|
||||
.cloned()
|
||||
.expect("project should have a task inventory")
|
||||
});
|
||||
|
||||
task_inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
TaskSettingsLocation::Global(Path::new("/tasks.json")),
|
||||
Some(
|
||||
&serde_json::to_string(&json!([
|
||||
// Tagged global task that should be scheduled from the Git graph context menu.
|
||||
{
|
||||
"label": "Git Show $ZED_GIT_SHA_SHORT",
|
||||
"command": "git",
|
||||
"args": ["show", "$ZED_GIT_SHA"],
|
||||
"cwd": "$ZED_GIT_REPOSITORY_PATH",
|
||||
"env": {
|
||||
"REPOSITORY": "$ZED_GIT_REPOSITORY_NAME",
|
||||
},
|
||||
"tags": [GIT_COMMAND_TASK_TAG],
|
||||
},
|
||||
// Untagged task that should not appear in the Git graph context menu.
|
||||
{
|
||||
"label": "Git Status",
|
||||
"command": "git",
|
||||
"args": ["status"],
|
||||
},
|
||||
// Tagged task that still should not appear because Git graph task contexts
|
||||
// do not provide editor-specific variables.
|
||||
{
|
||||
"label": "Print File $ZED_FILE",
|
||||
"command": "echo",
|
||||
"args": ["$ZED_FILE"],
|
||||
"tags": [GIT_COMMAND_TASK_TAG],
|
||||
},
|
||||
]))
|
||||
.expect("tasks JSON should serialize"),
|
||||
),
|
||||
)
|
||||
.expect("tasks should parse");
|
||||
});
|
||||
|
||||
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
workspace::MultiWorkspace::test_new(project.clone(), window, cx)
|
||||
});
|
||||
let workspace = multi_workspace.read_with(&*cx, |multi_workspace, _| {
|
||||
multi_workspace.workspace().clone()
|
||||
});
|
||||
let workspace_weak = workspace.downgrade();
|
||||
|
||||
let git_graph = cx.new_window_entity(|window, cx| {
|
||||
GitGraph::new(
|
||||
repository.read(cx).id,
|
||||
project.read(cx).git_store().clone(),
|
||||
workspace_weak,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(git_graph.clone()), None, true, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
git_graph.update_in(cx, |git_graph, window, cx| {
|
||||
assert_eq!(git_graph.graph_data.commits.len(), 1);
|
||||
git_graph.deploy_entry_context_menu(point(px(20.), px(20.)), 0, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let context_menu = git_graph.read_with(&*cx, |git_graph, _| {
|
||||
git_graph
|
||||
.context_menu
|
||||
.as_ref()
|
||||
.expect("context menu should be open")
|
||||
.menu
|
||||
.clone()
|
||||
});
|
||||
context_menu.update_in(cx, |context_menu, window, cx| {
|
||||
context_menu
|
||||
.select_last(window, cx)
|
||||
.expect("custom Git task should be selectable");
|
||||
context_menu.confirm(&menu::Confirm, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let (task_source_kind, resolved_task) = task_inventory.read_with(&*cx, |inventory, _| {
|
||||
inventory
|
||||
.last_scheduled_task(None)
|
||||
.expect("custom Git task should be scheduled")
|
||||
});
|
||||
|
||||
assert!(
|
||||
matches!(task_source_kind, TaskSourceKind::AbsPath { .. }),
|
||||
"scheduled task should come from global tasks"
|
||||
);
|
||||
assert_eq!(resolved_task.resolved_label, "Git Show abcdef1");
|
||||
assert_eq!(resolved_task.resolved.command, Some("git".to_string()));
|
||||
assert_eq!(
|
||||
resolved_task.resolved.args,
|
||||
vec![
|
||||
"show".to_string(),
|
||||
"abcdef1234567890abcdef1234567890abcdef12".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task.resolved.cwd,
|
||||
Some(Path::new("/project").to_path_buf())
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task.resolved.env.get("REPOSITORY"),
|
||||
Some(&"project".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -157,8 +157,8 @@ pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use prettier::RANGE_FORMAT_SUFFIX as TEST_PRETTIER_RANGE_FORMAT_SUFFIX;
|
||||
pub use task_inventory::{
|
||||
BasicContextProvider, ContextProviderWithTasks, DebugScenarioContext, Inventory, TaskContexts,
|
||||
TaskSourceKind,
|
||||
BasicContextProvider, ContextProviderWithTasks, DebugScenarioContext, GIT_COMMAND_TASK_TAG,
|
||||
Inventory, TaskContexts, TaskSourceKind,
|
||||
};
|
||||
|
||||
pub use buffer_store::ProjectTransaction;
|
||||
|
||||
@@ -30,6 +30,8 @@ use worktree::WorktreeId;
|
||||
|
||||
use crate::{git_store::GitStore, task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
|
||||
|
||||
pub const GIT_COMMAND_TASK_TAG: &str = "git-command";
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DebugScenarioContext {
|
||||
pub task_context: SharedTaskContext,
|
||||
@@ -644,6 +646,30 @@ impl Inventory {
|
||||
self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
|
||||
}
|
||||
|
||||
/// Returns global task templates with the provided tag.
|
||||
pub fn global_templates_with_tag(&self, tag: &str) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||
self.global_templates_from_settings()
|
||||
.filter(|(_, template)| template.tags.iter().any(|template_tag| template_tag == tag))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolves global task templates with the provided tag against the provided task context.
|
||||
pub fn resolve_global_tasks_with_tag(
|
||||
&self,
|
||||
tag: &str,
|
||||
task_context: &TaskContext,
|
||||
) -> Vec<(TaskSourceKind, ResolvedTask)> {
|
||||
self.global_templates_with_tag(tag)
|
||||
.into_iter()
|
||||
.filter_map(|(task_source_kind, task_template)| {
|
||||
let id_base = task_source_kind.to_id_base();
|
||||
task_template
|
||||
.resolve_task(&id_base, task_context)
|
||||
.map(|resolved_task| (task_source_kind, resolved_task))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns all task templates (worktree and global) that have at least one
|
||||
/// hook in the provided set.
|
||||
pub fn templates_with_hooks(
|
||||
|
||||
@@ -185,6 +185,14 @@ pub enum VariableName {
|
||||
/// For normal checkouts, this equals the worktree root. For linked worktrees,
|
||||
/// this is the original repo's working directory.
|
||||
MainGitWorktree,
|
||||
/// Full SHA for the Git commit associated with the task context.
|
||||
GitSha,
|
||||
/// Short SHA for the Git commit associated with the task context.
|
||||
GitShaShort,
|
||||
/// Name of the Git repository associated with the task context.
|
||||
GitRepositoryName,
|
||||
/// Absolute path of the Git repository associated with the task context.
|
||||
GitRepositoryPath,
|
||||
/// Custom variable, provided by the plugin or other external source.
|
||||
/// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
|
||||
Custom(Cow<'static, str>),
|
||||
@@ -221,6 +229,10 @@ impl FromStr for VariableName {
|
||||
"ROW" => Self::Row,
|
||||
"COLUMN" => Self::Column,
|
||||
"MAIN_GIT_WORKTREE" => Self::MainGitWorktree,
|
||||
"GIT_SHA" => Self::GitSha,
|
||||
"GIT_SHA_SHORT" => Self::GitShaShort,
|
||||
"GIT_REPOSITORY_NAME" => Self::GitRepositoryName,
|
||||
"GIT_REPOSITORY_PATH" => Self::GitRepositoryPath,
|
||||
_ => {
|
||||
if let Some(custom_name) =
|
||||
without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
|
||||
@@ -257,6 +269,10 @@ impl std::fmt::Display for VariableName {
|
||||
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
|
||||
Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"),
|
||||
Self::MainGitWorktree => write!(f, "{ZED_VARIABLE_NAME_PREFIX}MAIN_GIT_WORKTREE"),
|
||||
Self::GitSha => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_SHA"),
|
||||
Self::GitShaShort => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_SHA_SHORT"),
|
||||
Self::GitRepositoryName => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_REPOSITORY_NAME"),
|
||||
Self::GitRepositoryPath => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_REPOSITORY_PATH"),
|
||||
Self::Custom(s) => write!(
|
||||
f,
|
||||
"{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
|
||||
|
||||
@@ -511,7 +511,10 @@ fn substitute_all_template_variables_in_map(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{TaskVariables, VariableName};
|
||||
|
||||
@@ -1102,4 +1105,50 @@ mod tests {
|
||||
};
|
||||
assert!(task.unknown_variables().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_variables_resolution() {
|
||||
let task = TaskTemplate {
|
||||
label: "Show $ZED_GIT_SHA_SHORT in $ZED_GIT_REPOSITORY_NAME".to_string(),
|
||||
command: "git".to_string(),
|
||||
args: vec!["show".to_string(), "$ZED_GIT_SHA".to_string()],
|
||||
cwd: Some("$ZED_GIT_REPOSITORY_PATH".to_string()),
|
||||
env: HashMap::from_iter([("COMMIT".to_string(), "$ZED_GIT_SHA".to_string())]),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
let sha = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string();
|
||||
let sha_short = "0123456".to_string();
|
||||
let repo_name = "zed".to_string();
|
||||
let repo_path = format!("/Users/example/{repo_name}");
|
||||
|
||||
let context = TaskContext {
|
||||
task_variables: TaskVariables::from_iter([
|
||||
(VariableName::GitSha, sha.clone()),
|
||||
(VariableName::GitShaShort, sha_short.clone()),
|
||||
(VariableName::GitRepositoryName, repo_name.clone()),
|
||||
(VariableName::GitRepositoryPath, repo_path.clone()),
|
||||
]),
|
||||
..TaskContext::default()
|
||||
};
|
||||
|
||||
let task = task.resolve_task(TEST_ID_BASE, &context).unwrap();
|
||||
assert_eq!(
|
||||
task.resolved_label,
|
||||
format!("Show {sha_short} in {repo_name}")
|
||||
);
|
||||
assert_eq!(task.resolved.command, Some("git".to_string()));
|
||||
assert_eq!(task.resolved.args, vec!["show".to_string(), sha.clone()]);
|
||||
assert_eq!(task.resolved.cwd, Some(PathBuf::from(repo_path)));
|
||||
assert_eq!(task.resolved.env.get("COMMIT"), Some(&sha));
|
||||
|
||||
assert_substituted_variables(
|
||||
&task,
|
||||
vec![
|
||||
VariableName::GitSha,
|
||||
VariableName::GitShaShort,
|
||||
VariableName::GitRepositoryName,
|
||||
VariableName::GitRepositoryPath,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,34 @@ Hook tasks are resolved from the same global and worktree-local `tasks.json` fil
|
||||
|
||||
Tasks that define `hooks` are still available from the task modal like any other task, so the same template can be reused for manual runs.
|
||||
|
||||
## Custom Git Commands
|
||||
|
||||
The Git Graph supports running custom Git command tasks from the commit context menu.
|
||||
To add a command, define a task in your global `tasks.json` file with the `git-command` tag (worktree-local tasks are not supported yet).
|
||||
When shown from a commit's context menu, the task is resolved against the selected commit and repository, and runs from the selected repository root by default.
|
||||
|
||||
Git Graph command tasks support the Git-specific task variables below.
|
||||
These variables are provided only when resolving Git Graph command tasks.
|
||||
Other task variables, such as `ZED_FILE`, `ZED_SELECTED_TEXT`, `ZED_WORKTREE_ROOT`, and `ZED_MAIN_GIT_WORKTREE`, are not provided to Git Graph command tasks unless they use default values.
|
||||
|
||||
- `ZED_GIT_SHA`: full SHA of the selected commit.
|
||||
- `ZED_GIT_SHA_SHORT`: short SHA of the selected commit.
|
||||
- `ZED_GIT_REPOSITORY_NAME`: name of the selected Git repository.
|
||||
- `ZED_GIT_REPOSITORY_PATH`: absolute path to the selected Git repository's working directory.
|
||||
|
||||
For example:
|
||||
|
||||
```json [tasks]
|
||||
[
|
||||
{
|
||||
"label": "Branches containing commit: $ZED_GIT_SHA_SHORT",
|
||||
"command": "git",
|
||||
"args": ["branch", "-a", "--contains", "$ZED_GIT_SHA"],
|
||||
"tags": ["git-command"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## VS Code Task Format
|
||||
|
||||
When importing VS Code tasks from `.vscode/tasks.json`, you can omit the `label` field. Zed automatically generates labels based on the task type:
|
||||
|
||||
Reference in new issue
Block a user