git_ui: Move git_graph into git_ui (#57503)

cc @Anthony-Eid

## Why

This is the first step in moving the Git Graph work into the Git UI
crate before continuing with follow-up refactors and feature work. The
goal is for Git UI components and shared Git UI helpers to live in one
crate, so future changes to the Git Graph can reuse existing `git_ui`
code instead of duplicating it.

This PR is not only a filesystem move. While moving `git_graph` into
`git_ui`, a few small dependency and helper boundaries had to change:

- `git_graph` and `git_ui` both needed the same remote parsing and
commit tooltip construction behavior, so those pieces are now shared
from `git_ui`.
- `git_graph` previously depended on `project_panel` to resolve
file-history actions from the project panel selection. After moving
`git_graph` into `git_ui`, keeping that dependency would create an
undesirable `git_ui` -> `project_panel` relationship. The
project-panel-specific action forwarding now lives in `project_panel`,
and calls into exported `git_ui::git_graph` helpers instead.
- `git_graph` initialization now happens through `git_ui::init`, so
downstream crates only need to initialize `git_ui`.

This prepares the codebase for the next planned PRs: splitting the large
`git_graph.rs` implementation into smaller pieces, then adding Git Graph
features such as keeping the main branch lane at index `0`.

## License removal

The removed `crates/git_graph/LICENSE-GPL` file was a symlink to the
repository root `LICENSE-GPL`. The moved code is now inside `git_ui`,
which is also licensed as `GPL-3.0-or-later` and has its own
`LICENSE-GPL` symlink to the same root license file. The code did not
move to a differently licensed crate; it remains covered by the same GPL
license.

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:

- N/A

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
This commit is contained in:
Mikhail PertsevandAnthony Eid authored and GitHub committed 2026-06-02 19:13:17 +00:00
1 parent b369cc24b1
commit 9abfeab186
17 files changed
+451 -257

No files matched your search

Generated
+3 -36
View File
@@ -3373,7 +3373,6 @@ dependencies = [
"fs",
"futures 0.3.32",
"git",
"git_graph",
"git_hosting_providers",
"git_ui",
"gpui",
@@ -7279,40 +7278,6 @@ dependencies = [
"ztracing",
]
[[package]]
name = "git_graph"
version = "0.1.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"collections",
"db",
"editor",
"fs",
"git",
"git_ui",
"gpui",
"language",
"language_model",
"menu",
"picker",
"project",
"project_panel",
"rand 0.9.4",
"release_channel",
"remote_connection",
"search",
"serde_json",
"settings",
"smallvec",
"task",
"theme",
"theme_settings",
"time",
"ui",
"workspace",
]
[[package]]
name = "git_hosting_providers"
version = "0.1.0"
@@ -7342,6 +7307,7 @@ dependencies = [
"agent_settings",
"anyhow",
"askpass",
"async-channel 2.5.0",
"buffer_diff",
"call",
"collections",
@@ -7374,9 +7340,11 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.9.4",
"release_channel",
"remote",
"remote_connection",
"schemars 1.0.4",
"search",
"serde",
"serde_json",
"settings",
@@ -22887,7 +22855,6 @@ dependencies = [
"fs",
"futures 0.3.32",
"git",
"git_graph",
"git_hosting_providers",
"git_ui",
"go_to_line",
-2
View File
@@ -83,7 +83,6 @@ members = [
"crates/fuzzy",
"crates/fuzzy_nucleo",
"crates/git",
"crates/git_graph",
"crates/git_hosting_providers",
"crates/git_ui",
"crates/go_to_line",
@@ -339,7 +338,6 @@ fs = { path = "crates/fs" }
fuzzy = { path = "crates/fuzzy" }
fuzzy_nucleo = { path = "crates/fuzzy_nucleo" }
git = { path = "crates/git" }
git_graph = { path = "crates/git_graph" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
-1
View File
@@ -97,7 +97,6 @@ extension.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
git_graph = { workspace = true, features = ["test-support"] }
git_hosting_providers.workspace = true
git_ui = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
+2 -4
View File
@@ -11,7 +11,7 @@ use git::{
repository::{CommitData, InitialGraphCommitData, RepoPath, Worktree as GitWorktree},
status::{DiffStat, FileStatus, StatusCode, TrackedStatus},
};
use git_graph::GitGraph;
use git_ui::git_graph::GitGraph;
use git_ui::{git_panel::GitPanel, project_diff::ProjectDiff};
use gpui::{
AppContext as _, BackgroundExecutor, Entity, IntoElement as _, SharedString, TestAppContext,
@@ -763,11 +763,9 @@ async fn test_remote_git_graph_data_and_search(
.await;
cx_a.update(|cx| {
git_ui::init(cx);
git_graph::init(cx);
});
cx_b.update(|cx| {
git_ui::init(cx);
git_graph::init(cx);
});
let active_call_a = cx_a.read(ActiveCall::global);
@@ -781,7 +779,7 @@ async fn test_remote_git_graph_data_and_search(
let search_query = "graph search match";
let mut rng = StdRng::seed_from_u64(7);
let commits = git_graph::generate_random_commit_dag(&mut rng, 12, true);
let commits = git_ui::git_graph::generate_random_commit_dag(&mut rng, 12, true);
let dot_git = Path::new(path!("/project/.git"));
client_a.fs().set_graph_commits(dot_git, commits.clone());
-60
View File
@@ -1,60 +0,0 @@
[package]
name = "git_graph"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/git_graph.rs"
[features]
default = []
test-support = [
"dep:rand",
"project/test-support",
"gpui/test-support",
"remote_connection/test-support",
]
[dependencies]
anyhow.workspace = true
async-channel.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
git.workspace = true
git_ui.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
picker.workspace = true
project.workspace = true
project_panel.workspace = true
rand = { workspace = true, optional = true }
release_channel.workspace = true
search.workspace = true
settings.workspace = true
smallvec.workspace = true
task.workspace = true
theme.workspace = true
theme_settings.workspace = true
time.workspace = true
ui.workspace = true
workspace.workspace = true
[dev-dependencies]
db = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language_model.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
remote_connection = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
-1
View File
@@ -1 +0,0 @@
../../LICENSE-GPL
+7
View File
@@ -19,6 +19,7 @@ test-support = ["multi_buffer/test-support", "remote_connection/test-support"]
agent_settings.workspace = true
anyhow.workspace = true
askpass.workspace = true
async-channel.workspace = true
buffer_diff.workspace = true
call.workspace = true
collections.workspace = true
@@ -48,15 +49,18 @@ project.workspace = true
prompt_store.workspace = true
proto.workspace = true
rand.workspace = true
release_channel.workspace = true
remote_connection.workspace = true
remote.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
strum.workspace = true
sysinfo.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
theme.workspace = true
@@ -78,7 +82,10 @@ windows.workspace = true
[dev-dependencies]
ctor.workspace = true
db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
pretty_assertions.workspace = true
+1 -1
View File
@@ -1306,7 +1306,7 @@ impl Render for CommitViewToolbar {
.tooltip(Tooltip::text("Show in Git Graph"))
.on_click(move |_, window, cx| {
window.dispatch_action(
Box::new(crate::git_panel::OpenAtCommit {
Box::new(crate::git_graph::OpenAtCommit {
sha: sha_for_graph.clone(),
}),
cx,
@@ -1,3 +1,8 @@
use crate::{
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
commit_view::CommitView,
git_status_icon,
};
use collections::{BTreeMap, HashMap, IndexSet};
use editor::Editor;
use git::{
@@ -10,11 +15,6 @@ use git::{
},
status::{FileStatus, StatusCode, TrackedStatus},
};
use git_ui::{
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
commit_view::CommitView,
git_status_icon,
};
use gpui::{
Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength,
DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
@@ -33,7 +33,6 @@ use project::{
RepositoryEvent, RepositoryId,
},
};
use project_panel::ProjectPanel;
use search::{
SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch,
ToggleCaseSensitive, buffer_search,
@@ -340,24 +339,24 @@ struct SearchState {
case_sensitive: bool,
editor: Entity<Editor>,
state: QueryState,
pub matches: IndexSet<Oid>,
pub selected_index: Option<usize>,
matches: IndexSet<Oid>,
selected_index: Option<usize>,
}
pub struct SplitState {
struct SplitState {
left_ratio: f32,
visible_left_ratio: f32,
}
impl SplitState {
pub fn new() -> Self {
fn new() -> Self {
Self {
left_ratio: 1.0,
visible_left_ratio: 1.0,
}
}
pub fn right_ratio(&self) -> f32 {
fn right_ratio(&self) -> f32 {
1.0 - self.visible_left_ratio
}
@@ -391,6 +390,8 @@ impl SplitState {
actions!(
git_graph,
[
/// Opens the Git Graph Tab.
Open,
/// Copies the SHA of the selected commit to the clipboard.
CopyCommitSha,
/// Copies a tag from the selected commit to the clipboard.
@@ -410,6 +411,13 @@ actions!(
]
);
/// Opens the Git Graph Tab at a specific commit.
#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)]
#[action(namespace = git_graph)]
pub struct OpenAtCommit {
pub sha: String,
}
fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] {
static FORMAT: OnceLock<Vec<BorrowedFormatItem<'static>>> = OnceLock::new();
FORMAT.get_or_init(|| {
@@ -893,7 +901,7 @@ pub fn init(cx: &mut App) {
div.on_action({
let workspace = workspace.clone();
move |_: &git_ui::git_panel::Open, window, cx| {
move |_: &Open, window, cx| {
workspace
.update(cx, |workspace, cx| {
let Some(repo) =
@@ -918,33 +926,29 @@ pub fn init(cx: &mut App) {
.ok();
}
})
.on_action(
move |action: &git_ui::git_panel::OpenAtCommit, window, cx| {
let sha = action.sha.clone();
workspace
.update(cx, |workspace, cx| {
let Some(repo) =
workspace.project().read(cx).active_repository(cx)
else {
return;
};
let selected_repo_id = repo.read(cx).id;
.on_action(move |action: &OpenAtCommit, window, cx| {
let sha = action.sha.clone();
workspace
.update(cx, |workspace, cx| {
let Some(repo) = workspace.project().read(cx).active_repository(cx)
else {
return;
};
let selected_repo_id = repo.read(cx).id;
let git_store =
workspace.project().read(cx).git_store().clone();
open_or_reuse_graph(
workspace,
selected_repo_id,
git_store,
LogSource::All,
Some(sha),
window,
cx,
);
})
.ok();
},
)
let git_store = workspace.project().read(cx).git_store().clone();
open_or_reuse_graph(
workspace,
selected_repo_id,
git_store,
LogSource::All,
Some(sha),
window,
cx,
);
})
.ok();
})
},
)
});
@@ -952,28 +956,32 @@ pub fn init(cx: &mut App) {
.detach();
}
/// Resolves a `git::FileHistory` target from a known project path (used by
/// callers like `project_panel` that own a focused selection but cannot be
/// referenced from this module due to dependency direction).
pub fn resolve_file_history_target_from_project_path(
workspace: &Workspace,
project_path: &ProjectPath,
cx: &App,
) -> Option<(RepositoryId, LogSource)> {
let git_store = workspace.project().read(cx).git_store();
let (repo, repo_path) = git_store
.read(cx)
.repository_and_path_for_project_path(project_path, cx)?;
let log_source = if repo_path.is_empty() {
LogSource::All
} else {
LogSource::Path(repo_path)
};
Some((repo.read(cx).id, log_source))
}
fn resolve_file_history_target(
workspace: &Workspace,
window: &Window,
cx: &App,
) -> Option<(RepositoryId, LogSource)> {
if let Some(panel) = workspace.panel::<ProjectPanel>(cx)
&& panel.read(cx).focus_handle(cx).contains_focused(window, cx)
&& let Some(project_path) = panel.read(cx).selected_entry_project_path(cx)
{
let git_store = workspace.project().read(cx).git_store();
let (repo, repo_path) = git_store
.read(cx)
.repository_and_path_for_project_path(&project_path, cx)?;
let log_source = if repo_path.is_empty() {
LogSource::All
} else {
LogSource::Path(repo_path)
};
return Some((repo.read(cx).id, log_source));
}
if let Some(panel) = workspace.panel::<git_ui::git_panel::GitPanel>(cx)
if let Some(panel) = workspace.panel::<crate::git_panel::GitPanel>(cx)
&& panel.read(cx).focus_handle(cx).contains_focused(window, cx)
&& let Some((repository, repo_path)) = panel.read(cx).selected_file_history_target()
{
@@ -997,7 +1005,7 @@ fn resolve_file_history_target(
Some((repo.read(cx).id, LogSource::Path(repo_path)))
}
fn open_or_reuse_graph(
pub fn open_or_reuse_graph(
workspace: &mut Workspace,
repo_id: RepositoryId,
git_store: Entity<GitStore>,
@@ -2314,22 +2322,6 @@ impl GitGraph {
cx.notify();
}
fn get_remote(
&self,
repository: &Repository,
_window: &mut Window,
cx: &mut App,
) -> Option<GitRemote> {
let remote_url = repository.default_remote_url()?;
let provider_registry = GitHostingProviderRegistry::default_global(cx);
let (provider, parsed) = parse_git_remote_url(provider_registry, &remote_url)?;
Some(GitRemote {
host: provider,
owner: parsed.owner.into(),
repo: parsed.repo.into(),
})
}
fn render_search_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
let color = cx.theme().colors();
let query_focus_handle = self
@@ -2536,7 +2528,16 @@ impl GitGraph {
})
.unwrap_or_default();
let remote = repository.update(cx, |repo, cx| self.get_remote(repo, window, cx));
let remote = repository.update(cx, |repo, cx| {
let remote_url = repo.default_remote_url()?;
let provider_registry = GitHostingProviderRegistry::default_global(cx);
let (provider, parsed) = parse_git_remote_url(provider_registry, &remote_url)?;
Some(GitRemote {
host: provider,
owner: parsed.owner.into(),
repo: parsed.repo.into(),
})
});
let avatar = {
let author_email_for_avatar = if author_email.is_empty() {
@@ -2733,7 +2734,7 @@ impl GitGraph {
})
.when_some(remote.clone(), |this, remote| {
let provider_name = remote.host.name();
let icon = git_ui::get_provider_icon(provider_name.as_str());
let icon = crate::get_provider_icon(provider_name.as_str());
let parsed_remote = ParsedGitRemote {
owner: remote.owner.as_ref().into(),
repo: remote.repo.as_ref().into(),
@@ -4084,6 +4085,11 @@ impl GitGraph {
.map(|commit| commit.data.clone())
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn log_source_for_test(&self) -> &LogSource {
&self.log_source
}
}
/// Generates a random commit DAG suitable for testing git graph rendering.
@@ -4219,9 +4225,7 @@ mod tests {
cx.set_global(settings_store);
theme_settings::init(theme::LoadThemes::JustBase, cx);
language_model::init(cx);
git_ui::init(cx);
project_panel::init(cx);
init(cx);
crate::init(cx);
});
}
@@ -4794,24 +4798,14 @@ mod tests {
});
repository.update(cx, |repo, cx| {
repo.graph_data(
crate::LogSource::default(),
crate::LogOrder::default(),
0..usize::MAX,
cx,
);
repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx);
});
cx.run_until_parked();
let graph_commits: Vec<Arc<InitialGraphCommitData>> = repository.update(cx, |repo, cx| {
repo.graph_data(
crate::LogSource::default(),
crate::LogOrder::default(),
0..usize::MAX,
cx,
)
.commits
.to_vec()
repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx)
.commits
.to_vec()
});
let mut graph_data = GraphData::new(8);
@@ -4865,12 +4859,7 @@ mod tests {
});
repository.update(cx, |repo, cx| {
repo.graph_data(
crate::LogSource::default(),
crate::LogOrder::default(),
0..usize::MAX,
cx,
);
repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx);
});
project
@@ -4888,7 +4877,7 @@ mod tests {
"initial repository scan should emit HeadChanged"
);
let commit_count_after = repository.read_with(cx, |repo, _| {
repo.get_graph_data(crate::LogSource::default(), crate::LogOrder::default())
repo.get_graph_data(LogSource::default(), LogOrder::default())
.map(|data| data.commit_data.len())
.unwrap()
});
@@ -4927,18 +4916,13 @@ mod tests {
});
repository.update(cx, |repo, cx| {
repo.graph_data(
crate::LogSource::default(),
crate::LogOrder::default(),
0..usize::MAX,
cx,
);
repo.graph_data(LogSource::default(), LogOrder::default(), 0..usize::MAX, cx);
});
cx.run_until_parked();
let error = repository.read_with(cx, |repo, _| {
repo.get_graph_data(crate::LogSource::default(), crate::LogOrder::default())
repo.get_graph_data(LogSource::default(), LogOrder::default())
.and_then(|data| data.error.clone())
});
@@ -5070,14 +5054,12 @@ mod tests {
}
#[gpui::test]
async fn test_file_history_action_uses_focused_source_and_reuses_matching_graph(
cx: &mut TestAppContext,
) {
async fn test_file_history_action_uses_git_panel_and_editor_sources(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
Path::new("/project"),
Path::new(util::path!("/project")),
json!({
".git": {},
"tracked1.txt": "tracked 1",
@@ -5085,15 +5067,22 @@ mod tests {
}),
)
.await;
fs.set_status_for_repo(
Path::new(util::path!("/project/.git")),
&[
("tracked1.txt", StatusCode::Modified.worktree()),
("tracked2.txt", StatusCode::Modified.worktree()),
],
);
let commits = vec![Arc::new(InitialGraphCommitData {
sha: Oid::from_bytes(&[1; 20]).unwrap(),
parents: smallvec![],
ref_names: vec!["HEAD".into(), "refs/heads/main".into()],
})];
fs.set_graph_commits(Path::new("/project/.git"), commits);
fs.set_graph_commits(Path::new(util::path!("/project/.git")), commits);
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
let project = Project::test(fs.clone(), [Path::new(util::path!("/project"))], cx).await;
cx.run_until_parked();
let repository = project.read_with(cx, |project, cx| {
@@ -5127,18 +5116,10 @@ mod tests {
})
.expect("window should be available");
cx.background_executor.allow_parking();
let project_panel = cx
.foreground_executor()
.clone()
.block_test(ProjectPanel::load(
weak_workspace.clone(),
async_window_cx.clone(),
))
.expect("project panel should load");
let git_panel = cx
.foreground_executor()
.clone()
.block_test(git_ui::git_panel::GitPanel::load(
.block_test(crate::git_panel::GitPanel::load(
weak_workspace,
async_window_cx,
))
@@ -5149,21 +5130,20 @@ mod tests {
.update(cx, |multi, window, cx| {
let workspace = multi.workspace();
workspace.update(cx, |workspace, cx| {
workspace.add_panel(project_panel.clone(), window, cx);
workspace.add_panel(git_panel.clone(), window, cx);
});
})
.expect("workspace window should be available");
cx.executor().advance_clock(Duration::from_millis(100));
cx.run_until_parked();
workspace_window
.update(cx, |multi, window, cx| {
let workspace = multi.workspace();
project_panel.update(cx, |panel, cx| {
panel.select_path_for_test(tracked1.clone(), cx)
.update(cx, |_, window, cx| {
git_panel.update(cx, |panel, cx| {
panel.select_entry_by_path(tracked1.clone(), window, cx);
});
workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<ProjectPanel>(window, cx);
git_panel.update(cx, |panel, cx| {
panel.focus_handle(cx).focus(window, cx);
});
})
.expect("workspace window should be available");
@@ -5185,13 +5165,12 @@ mod tests {
});
workspace_window
.update(cx, |multi, window, cx| {
let workspace = multi.workspace();
.update(cx, |_, window, cx| {
git_panel.update(cx, |panel, cx| {
panel.select_entry_by_path(tracked1.clone(), window, cx);
});
workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<git_ui::git_panel::GitPanel>(window, cx);
git_panel.update(cx, |panel, cx| {
panel.focus_handle(cx).focus(window, cx);
});
})
.expect("workspace window should be available");
+6 -17
View File
@@ -134,21 +134,6 @@ actions!(
]
);
actions!(
git_graph,
[
/// Opens the Git Graph Tab.
Open,
]
);
/// Opens the Git Graph Tab at a specific commit.
#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)]
#[action(namespace = git_graph)]
pub struct OpenAtCommit {
pub sha: String,
}
fn prompt<T>(
msg: &str,
detail: Option<&str>,
@@ -5027,10 +5012,14 @@ impl GitPanel {
IconButton::new("git-graph-button", IconName::GitGraph)
.icon_size(IconSize::Small)
.tooltip(|_window, cx| {
Tooltip::for_action("Open Git Graph", &Open, cx)
Tooltip::for_action(
"Open Git Graph",
&crate::git_graph::Open,
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(Open.boxed_clone(), cx)
window.dispatch_action(crate::git_graph::Open.boxed_clone(), cx)
}),
),
),
+2
View File
@@ -36,6 +36,7 @@ pub mod commit_tooltip;
pub mod commit_view;
mod conflict_view;
pub mod file_diff_view;
pub mod git_graph;
pub mod git_panel;
mod git_panel_settings;
pub mod git_picker;
@@ -69,6 +70,7 @@ pub fn get_provider_icon(name: &str) -> IconName {
pub fn init(cx: &mut App) {
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
commit_view::init(cx);
git_graph::init(cx);
cx.observe_new(|editor: &mut Editor, _, cx| {
conflict_view::register_editor(editor, editor.buffer().clone(), cx);
+1
View File
@@ -55,6 +55,7 @@ fs.workspace = true
client = { workspace = true, features = ["test-support"] }
criterion.workspace = true
editor = { workspace = true, features = ["test-support"] }
git_ui = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
remote_connection = { workspace = true, features = ["test-support"] }
+44
View File
@@ -527,6 +527,50 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.delete(action, window, cx));
}
});
// Forwards `git::FileHistory` to `git_ui::git_graph` when the project
// panel is the focused source of selection. Lives here (and not in
// `git_ui`) so that `git_ui` does not need to depend on
// `project_panel`, which would create a dependency cycle.
workspace.register_action_renderer(|div, workspace, window, cx| {
let Some(panel) = workspace.panel::<ProjectPanel>(cx) else {
return div;
};
if !panel.read(cx).focus_handle(cx).contains_focused(window, cx) {
return div;
}
if panel.read(cx).selected_entry_project_path(cx).is_none() {
return div;
}
let workspace = workspace.weak_handle();
div.capture_action(move |_: &git::FileHistory, window, cx| {
workspace
.update(cx, |workspace, cx| {
let Some(panel) = workspace.panel::<ProjectPanel>(cx) else {
return;
};
let Some(project_path) = panel.read(cx).selected_entry_project_path(cx)
else {
return;
};
let Some((repo_id, log_source)) =
git_ui::git_graph::resolve_file_history_target_from_project_path(
workspace,
&project_path,
cx,
)
else {
return;
};
let git_store = workspace.project().read(cx).git_store().clone();
git_ui::git_graph::open_or_reuse_graph(
workspace, repo_id, git_store, log_source, None, window, cx,
);
})
.log_err();
cx.stop_propagation();
})
});
})
.detach();
}
+275 -1
View File
@@ -1,13 +1,18 @@
use super::*;
// use crate::undo::tests::{build_create_operation, build_rename_operation};
use collections::HashSet;
use editor::MultiBufferOffset;
use editor::{Editor, MultiBufferOffset};
use git::{
Oid,
repository::{InitialGraphCommitData, LogSource, RepoPath},
};
use gpui::{Empty, Entity, TestAppContext, VisualTestContext};
use menu::Cancel;
use pretty_assertions::assert_eq;
use project::{FakeFs, ProjectPath};
use serde_json::json;
use settings::{ProjectPanelAutoOpenSettings, SettingsStore};
use smallvec::smallvec;
use std::path::{Path, PathBuf};
use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::{
@@ -167,6 +172,254 @@ async fn test_opening_file(cx: &mut gpui::TestAppContext) {
ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
}
#[gpui::test]
async fn test_file_history_action_uses_focused_project_panel_selection(
cx: &mut gpui::TestAppContext,
) {
init_test_with_git_ui(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
Path::new("/project"),
json!({
".git": {},
"tracked1.txt": "tracked 1",
"tracked2.txt": "tracked 2",
}),
)
.await;
let commits = vec![Arc::new(InitialGraphCommitData {
sha: Oid::from_bytes(&[1; 20]).unwrap(),
parents: smallvec![],
ref_names: vec!["HEAD".into(), "refs/heads/main".into()],
})];
fs.set_graph_commits(Path::new("/project/.git"), commits);
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("should have active repository")
});
let project_panel_repo_path = RepoPath::new(&"tracked1.txt").unwrap();
let editor_repo_path = RepoPath::new(&"tracked2.txt").unwrap();
let project_panel_path = repository
.read_with(cx, |repository, cx| {
repository.repo_path_to_project_path(&project_panel_repo_path, cx)
})
.expect("project panel path should resolve");
let editor_path = repository
.read_with(cx, |repository, cx| {
repository.repo_path_to_project_path(&editor_repo_path, cx)
})
.expect("editor path should resolve");
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(&*cx, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
let project_panel = multi_workspace.update_in(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace();
workspace.update(cx, |workspace, cx| {
let project_panel = ProjectPanel::new(workspace, window, cx);
workspace.add_panel(project_panel.clone(), window, cx);
project_panel
})
});
cx.run_until_parked();
let editor_buffer = project
.update(cx, |project, cx| {
project.open_buffer(editor_path.clone(), cx)
})
.await
.expect("editor buffer should open");
multi_workspace.update_in(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace();
let multibuffer = cx.new(|cx| {
let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite);
multibuffer.set_excerpts_for_buffer(
editor_buffer.clone(),
[Default::default()..editor_buffer.read(cx).max_point()],
0,
cx,
);
multibuffer
});
let editor =
cx.new(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx));
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
});
editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx), cx);
});
});
cx.run_until_parked();
multi_workspace.update_in(cx, |_multi_workspace, window, cx| {
project_panel.update(cx, |panel, cx| {
panel.select_path_for_test(project_panel_path.clone(), cx);
});
project_panel.update(cx, |panel, cx| {
panel.focus_handle(cx).focus(window, cx);
});
});
cx.run_until_parked();
cx.update(|window, cx| {
window.dispatch_action(Box::new(git::FileHistory), cx);
});
cx.run_until_parked();
workspace.read_with(&*cx, |workspace, cx| {
let graphs = workspace
.items_of_type::<git_ui::git_graph::GitGraph>(cx)
.collect::<Vec<_>>();
assert_eq!(graphs.len(), 1);
assert_eq!(
graphs[0].read(cx).log_source_for_test(),
&LogSource::Path(project_panel_repo_path)
);
});
}
#[gpui::test]
async fn test_file_history_action_does_not_fall_back_to_editor_when_focused_project_panel_selection_has_no_git_repo(
cx: &mut gpui::TestAppContext,
) {
init_test_with_git_ui(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
Path::new("/git-project"),
json!({
".git": {},
"tracked.txt": "tracked",
}),
)
.await;
fs.insert_tree(
Path::new("/plain-project"),
json!({
"plain.txt": "plain",
}),
)
.await;
fs.set_graph_commits(
Path::new("/git-project/.git"),
vec![Arc::new(InitialGraphCommitData {
sha: Oid::from_bytes(&[1; 20]).unwrap(),
parents: smallvec![],
ref_names: vec!["HEAD".into(), "refs/heads/main".into()],
})],
);
let project = Project::test(
fs.clone(),
[Path::new("/git-project"), Path::new("/plain-project")],
cx,
)
.await;
cx.run_until_parked();
let repository = project.read_with(cx, |project, cx| {
project
.active_repository(cx)
.expect("should have active repository")
});
let editor_repo_path = RepoPath::new(&"tracked.txt").unwrap();
let editor_path = repository
.read_with(cx, |repository, cx| {
repository.repo_path_to_project_path(&editor_repo_path, cx)
})
.expect("editor path should resolve");
let plain_worktree_id = project.read_with(cx, |project, cx| {
project
.worktree_for_root_name("plain-project", cx)
.expect("plain worktree should exist")
.read(cx)
.id()
});
let plain_project_path = ProjectPath {
worktree_id: plain_worktree_id,
path: rel_path("plain.txt").into(),
};
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(&*cx, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
let project_panel = multi_workspace.update_in(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace();
workspace.update(cx, |workspace, cx| {
let project_panel = ProjectPanel::new(workspace, window, cx);
workspace.add_panel(project_panel.clone(), window, cx);
project_panel
})
});
cx.run_until_parked();
let editor_buffer = project
.update(cx, |project, cx| {
project.open_buffer(editor_path.clone(), cx)
})
.await
.expect("editor buffer should open");
multi_workspace.update_in(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace();
let multibuffer = cx.new(|cx| {
let mut multibuffer = editor::MultiBuffer::new(language::Capability::ReadWrite);
multibuffer.set_excerpts_for_buffer(
editor_buffer.clone(),
[Default::default()..editor_buffer.read(cx).max_point()],
0,
cx,
);
multibuffer
});
let editor =
cx.new(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx));
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
});
editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx), cx);
});
});
cx.run_until_parked();
multi_workspace.update_in(cx, |_multi_workspace, window, cx| {
project_panel.update(cx, |panel, cx| {
panel.select_path_for_test(plain_project_path.clone(), cx);
});
project_panel.update(cx, |panel, cx| {
panel.focus_handle(cx).focus(window, cx);
});
});
cx.run_until_parked();
cx.update(|window, cx| {
window.dispatch_action(Box::new(git::FileHistory), cx);
});
cx.run_until_parked();
workspace.read_with(&*cx, |workspace, cx| {
assert_eq!(
workspace
.items_of_type::<git_ui::git_graph::GitGraph>(cx)
.count(),
0
);
});
}
#[gpui::test]
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -10412,6 +10665,27 @@ fn init_test_with_editor(cx: &mut TestAppContext) {
});
}
fn init_test_with_git_ui(cx: &mut TestAppContext) {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
git_ui::init(cx);
crate::init(cx);
workspace::init(app_state, cx);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings
.project_panel
.get_or_insert_default()
.auto_fold_dirs = Some(false);
settings.project.worktree.file_scan_exclusions = Some(Vec::new())
});
});
});
}
fn set_auto_open_settings(
cx: &mut TestAppContext,
auto_open_settings: ProjectPanelAutoOpenSettings,
-1
View File
@@ -113,7 +113,6 @@ file_finder.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
git_graph.workspace = true
git_hosting_providers.workspace = true
git_ui.workspace = true
go_to_line.workspace = true
-1
View File
@@ -767,7 +767,6 @@ fn main() {
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
git_ui::init(cx);
git_graph::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
csv_preview::init(cx);
-1
View File
@@ -5478,7 +5478,6 @@ mod tests {
);
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
web_search::init(cx);
git_graph::init(cx);
web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx);
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
project::AgentRegistryStore::init_global(