@@ -2639,338 +2639,86 @@ async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContex
}
#[gpui::test]
-async fn test_worktree_add_and_remove_migrates_threads(cx: &mut TestAppContext) {
- // When a worktree is added to a project, the project group key changes
- // and all historical threads should be migrated to the new key. Removing
- // the worktree should migrate them back.
- let (_fs, project) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
+async fn test_group_level_folder_add_cascades_to_all_workspaces(cx: &mut TestAppContext) {
+ // Group-level operations (add_folders_to_project_group) should cascade
+ // to all workspaces in the group and update the group key in-place.
+ // The group identity (ID) stays the same.
+ let (fs, project_a) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Save two threads against the initial project group [/project-a].
- save_n_test_threads(2, &project, cx).await;
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread 2",
- " Thread 1",
- ]
- );
-
- // Verify the metadata store has threads under the old key.
- let old_key_paths = PathList::new(&[PathBuf::from("/project-a")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key before add"
- );
- });
-
- // Add a second worktree to the same project.
- project
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/project-b", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // The project group key should now be [/project-a, /project-b].
- let new_key_paths = PathList::new(&[PathBuf::from("/project-a"), PathBuf::from("/project-b")]);
-
- // Verify multi-workspace state: exactly one project group key, the new one.
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after add"
- );
- assert_eq!(
- keys[0].path_list(),
- &new_key_paths,
- "the key should be the new combined path list"
- );
- });
-
- // Verify threads were migrated to the new key.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 0,
- "should have 0 threads under old key after migration"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 2,
- "should have 2 threads under new key after migration"
- );
- });
-
- // Sidebar should show threads under the new header.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread 2",
- " Thread 1",
- ]
- );
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+ let _sidebar = setup_sidebar(&multi_workspace, cx);
- // Now remove the second worktree.
- let worktree_id = project.read_with(cx, |project, cx| {
- project
- .visible_worktrees(cx)
- .find(|wt| wt.read(cx).abs_path().as_ref() == Path::new("/project-b"))
- .map(|wt| wt.read(cx).id())
- .expect("should find project-b worktree")
- });
- project.update(cx, |project, cx| {
- project.remove_worktree(worktree_id, cx);
+ // Create a second workspace with its own project in the same group.
+ let project_b =
+ project::Project::test(fs.clone() as Arc<dyn Fs>, ["/project-a".as_ref()], cx).await;
+ let _workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.test_add_workspace(project_b.clone(), window, cx)
});
cx.run_until_parked();
- // The key should revert to [/project-a].
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after remove"
- );
- assert_eq!(
- keys[0].path_list(),
- &old_key_paths,
- "the key should revert to the original path list"
- );
- });
-
- // Threads should be migrated back to the old key.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 0,
- "should have 0 threads under new key after revert"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key after revert"
- );
+ // Both workspaces should be in one group with key [/project-a].
+ let group_id = multi_workspace.read_with(cx, |mw, _| {
+ assert_eq!(mw.project_groups().len(), 1);
+ assert_eq!(mw.project_groups()[0].workspaces.len(), 2);
+ mw.project_groups()[0].id
});
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread 2",
- " Thread 1",
- ]
- );
-}
-
-#[gpui::test]
-async fn test_worktree_add_and_remove_preserves_thread_path_associations(cx: &mut TestAppContext) {
- // Verifies that adding/removing folders to a project correctly updates
- // each thread's worktree_paths (both folder_paths and main_worktree_paths)
- // while preserving per-path associations for linked worktrees.
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- "/project",
- serde_json::json!({
- ".git": {},
- "src": {},
- }),
- )
- .await;
- fs.add_linked_worktree_for_repo(
- Path::new("/project/.git"),
- false,
- git::repository::Worktree {
- path: PathBuf::from("/wt-feature"),
- ref_name: Some("refs/heads/feature".into()),
- sha: "aaa".into(),
- is_main: false,
- },
- )
- .await;
- fs.insert_tree("/other-project", serde_json::json!({ ".git": {} }))
- .await;
- cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
-
- // Start with a linked worktree workspace: visible root is /wt-feature,
- // main repo is /project.
- let project =
- project::Project::test(fs.clone() as Arc<dyn Fs>, ["/wt-feature".as_ref()], cx).await;
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let _sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Save a thread. It should have folder_paths=[/wt-feature], main=[/project].
- save_named_thread_metadata("thread-1", "Thread 1", &project, cx).await;
-
- let session_id = acp::SessionId::new(Arc::from("thread-1"));
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- let thread = store.entry(&session_id).expect("thread should exist");
- assert_eq!(
- thread.folder_paths().paths(),
- &[PathBuf::from("/wt-feature")],
- "initial folder_paths should be the linked worktree"
- );
- assert_eq!(
- thread.main_worktree_paths().paths(),
- &[PathBuf::from("/project")],
- "initial main_worktree_paths should be the main repo"
- );
+ // Add /project-b via the group-level API.
+ multi_workspace.update(cx, |mw, cx| {
+ mw.add_folders_to_project_group(group_id, vec![PathBuf::from("/project-b")], cx);
});
-
- // Add /other-project to the workspace.
- project
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/other-project", true, cx)
- })
- .await
- .expect("should add worktree");
cx.run_until_parked();
- // Thread should now have both paths, with correct associations.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- let thread = store.entry(&session_id).expect("thread should exist");
- let pairs: Vec<_> = thread
- .worktree_paths
- .ordered_pairs()
- .map(|(m, f)| (m.clone(), f.clone()))
- .collect();
- assert!(
- pairs.contains(&(PathBuf::from("/project"), PathBuf::from("/wt-feature"))),
- "linked worktree association should be preserved, got: {:?}",
- pairs
- );
+ // The group key should be updated.
+ multi_workspace.read_with(cx, |mw, _| {
+ assert_eq!(mw.project_groups().len(), 1, "still one group");
+ assert_eq!(mw.project_groups()[0].id, group_id, "same group ID");
+ let paths = mw.project_groups()[0].key.path_list().paths().to_vec();
assert!(
- pairs.contains(&(
- PathBuf::from("/other-project"),
- PathBuf::from("/other-project")
- )),
- "new folder should have main == folder, got: {:?}",
- pairs
+ paths.contains(&PathBuf::from("/project-a"))
+ && paths.contains(&PathBuf::from("/project-b")),
+ "group key should contain both paths, got {:?}",
+ paths,
);
});
- // Remove /other-project.
- let worktree_id = project.read_with(cx, |project, cx| {
- project
- .visible_worktrees(cx)
- .find(|wt| wt.read(cx).abs_path().as_ref() == Path::new("/other-project"))
- .map(|wt| wt.read(cx).id())
- .expect("should find other-project worktree")
- });
- project.update(cx, |project, cx| {
- project.remove_worktree(worktree_id, cx);
- });
- cx.run_until_parked();
-
- // Thread should be back to original state.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- let thread = store.entry(&session_id).expect("thread should exist");
- assert_eq!(
- thread.folder_paths().paths(),
- &[PathBuf::from("/wt-feature")],
- "folder_paths should revert to just the linked worktree"
- );
- assert_eq!(
- thread.main_worktree_paths().paths(),
- &[PathBuf::from("/project")],
- "main_worktree_paths should revert to just the main repo"
- );
- let pairs: Vec<_> = thread
- .worktree_paths
- .ordered_pairs()
- .map(|(m, f)| (m.clone(), f.clone()))
- .collect();
- assert_eq!(
- pairs,
- vec![(PathBuf::from("/project"), PathBuf::from("/wt-feature"))],
- "linked worktree association should be preserved through add+remove cycle"
- );
- });
+ // Both workspaces should have gotten the new worktree.
+ let worktree_count_a = project_a.read_with(cx, |p, cx| p.visible_worktrees(cx).count());
+ let worktree_count_b = project_b.read_with(cx, |p, cx| p.visible_worktrees(cx).count());
+ assert_eq!(worktree_count_a, 2, "project A should have 2 worktrees");
+ assert_eq!(worktree_count_b, 2, "project B should have 2 worktrees");
}
#[gpui::test]
-async fn test_worktree_add_key_collision_removes_duplicate_workspace(cx: &mut TestAppContext) {
- // When a worktree is added to workspace A and the resulting key matches
- // an existing workspace B's key (and B has the same root paths), B
- // should be removed as a true duplicate.
+async fn test_individual_workspace_folder_change_moves_workspace_to_new_group(
+ cx: &mut TestAppContext,
+) {
+ // When a folder is added to an individual workspace (not via the group
+ // API), that workspace should move to a new or existing group matching
+ // its new key. Sibling workspaces in the old group are NOT affected.
let (fs, project_a) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- let workspace_a = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
-
- // Save a thread against workspace A [/project-a].
- save_named_thread_metadata("thread-a", "Thread A", &project_a, cx).await;
+ let _sidebar = setup_sidebar(&multi_workspace, cx);
- // Create workspace B with both worktrees [/project-a, /project-b].
- let project_b = project::Project::test(
- fs.clone() as Arc<dyn Fs>,
- ["/project-a".as_ref(), "/project-b".as_ref()],
- cx,
- )
- .await;
- let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ // Create a second workspace that shares the same group.
+ let project_b =
+ project::Project::test(fs.clone() as Arc<dyn Fs>, ["/project-a".as_ref()], cx).await;
+ let _workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_b.clone(), window, cx)
});
cx.run_until_parked();
- // Switch back to workspace A so it's the active workspace when the collision happens.
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.activate(workspace_a, window, cx);
+ multi_workspace.read_with(cx, |mw, _| {
+ assert_eq!(mw.project_groups().len(), 1, "one group to start");
+ assert_eq!(
+ mw.project_groups()[0].workspaces.len(),
+ 2,
+ "two workspaces in it"
+ );
});
- cx.run_until_parked();
-
- // Save a thread against workspace B [/project-a, /project-b].
- save_named_thread_metadata("thread-b", "Thread B", &project_b, cx).await;
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- // Both project groups should be visible.
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread B",
- "v [project-a]",
- " Thread A",
- ]
- );
-
- let workspace_b_id = workspace_b.entity_id();
- // Now add /project-b to workspace A's project, causing a key collision.
+ // Add /project-b to project_a directly (individual workspace change).
project_a
.update(cx, |project, cx| {
project.find_or_create_worktree("/project-b", true, cx)
@@ -2979,181 +2727,58 @@ async fn test_worktree_add_key_collision_removes_duplicate_workspace(cx: &mut Te
.expect("should add worktree");
cx.run_until_parked();
- // Workspace B should have been removed (true duplicate — same root paths).
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- !workspace_ids.contains(&workspace_b_id),
- "workspace B should have been removed after key collision"
- );
- });
-
- // There should be exactly one project group key now.
- let combined_paths = PathList::new(&[PathBuf::from("/project-a"), PathBuf::from("/project-b")]);
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after collision"
- );
+ // project_a's workspace should have moved to a new group.
+ // project_b's workspace should stay in the old group, unchanged.
+ multi_workspace.read_with(cx, |mw, _| {
+ assert_eq!(mw.project_groups().len(), 2, "should now have 2 groups");
+ let mut group_sizes: Vec<usize> = mw
+ .project_groups()
+ .iter()
+ .map(|g| g.workspaces.len())
+ .collect();
+ group_sizes.sort();
assert_eq!(
- keys[0].path_list(),
- &combined_paths,
- "the remaining key should be the combined paths"
+ group_sizes,
+ vec![1, 1],
+ "each group should have 1 workspace"
);
});
- // Both threads should be visible under the merged group.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
+ // project_b should still have only 1 worktree (no cascading).
+ let worktree_count_b = project_b.read_with(cx, |p, cx| p.visible_worktrees(cx).count());
assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread A",
- " Thread B",
- ]
+ worktree_count_b, 1,
+ "sibling workspace should NOT get the new folder"
);
}
#[gpui::test]
-async fn test_worktree_collision_keeps_active_workspace(cx: &mut TestAppContext) {
- // When workspace A adds a folder that makes it collide with workspace B,
- // and B is the *active* workspace, A (the incoming one) should be
- // dropped so the user stays on B. A linked worktree sibling of A
- // should migrate into B's group.
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- // Set up /project-a with a linked worktree.
- fs.insert_tree(
- "/project-a",
- serde_json::json!({
- ".git": {
- "worktrees": {
- "feature": {
- "commondir": "../../",
- "HEAD": "ref: refs/heads/feature",
- },
- },
- },
- "src": {},
- }),
- )
- .await;
- fs.insert_tree(
- "/wt-feature",
- serde_json::json!({
- ".git": "gitdir: /project-a/.git/worktrees/feature",
- "src": {},
- }),
- )
- .await;
- fs.add_linked_worktree_for_repo(
- Path::new("/project-a/.git"),
- false,
- git::repository::Worktree {
- path: PathBuf::from("/wt-feature"),
- ref_name: Some("refs/heads/feature".into()),
- sha: "aaa".into(),
- is_main: false,
- },
- )
- .await;
- fs.insert_tree("/project-b", serde_json::json!({ ".git": {}, "src": {} }))
- .await;
- cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
-
- let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
- project_a.update(cx, |p, cx| p.git_scans_complete(cx)).await;
-
- // Linked worktree sibling of A.
- let project_wt = project::Project::test(fs.clone(), ["/wt-feature".as_ref()], cx).await;
- project_wt
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
+async fn test_individual_workspace_change_merges_into_existing_group(cx: &mut TestAppContext) {
+ // When an individual workspace's key changes to match an existing group,
+ // it should move into that group (natural merge).
+ let (fs, project_a) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+ let _sidebar = setup_sidebar(&multi_workspace, cx);
- // Workspace B has both folders already.
+ // Create workspace B with both folders [/project-a, /project-b].
let project_b = project::Project::test(
fs.clone() as Arc<dyn Fs>,
["/project-a".as_ref(), "/project-b".as_ref()],
cx,
)
.await;
-
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Add agent panels to all workspaces.
- let workspace_a_entity = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
- add_agent_panel(&workspace_a_entity, cx);
-
- // Add the linked worktree workspace (sibling of A).
- let workspace_wt = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_wt.clone(), window, cx)
- });
- add_agent_panel(&workspace_wt, cx);
- cx.run_until_parked();
-
- // Add workspace B (will become active).
- let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ let _workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_b.clone(), window, cx)
});
- add_agent_panel(&workspace_b, cx);
cx.run_until_parked();
- // Save threads in each group.
- save_named_thread_metadata("thread-a", "Thread A", &project_a, cx).await;
- save_thread_metadata_with_main_paths(
- "thread-wt",
- "Worktree Thread",
- PathList::new(&[PathBuf::from("/wt-feature")]),
- PathList::new(&[PathBuf::from("/project-a")]),
- cx,
- );
- save_named_thread_metadata("thread-b", "Thread B", &project_b, cx).await;
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- // B is active, A and wt-feature are in one group, B in another.
- assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspace().entity_id()),
- workspace_b.entity_id(),
- "workspace B should be active"
- );
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(mw.project_group_keys().count(), 2, "should have 2 groups");
- assert_eq!(mw.workspaces().count(), 3, "should have 3 workspaces");
+ // Should have 2 groups: one [/project-a], one [/project-a, /project-b].
+ multi_workspace.read_with(cx, |mw, _| {
+ assert_eq!(mw.project_groups().len(), 2);
});
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread B",
- "v [project-a]",
- " Thread A",
- " Worktree Thread {wt-feature}",
- ]
- );
-
- let workspace_a = multi_workspace.read_with(cx, |mw, _| {
- mw.workspaces()
- .find(|ws| {
- ws.entity_id() != workspace_b.entity_id()
- && ws.entity_id() != workspace_wt.entity_id()
- })
- .unwrap()
- .clone()
- });
-
- // Add /project-b to workspace A's project, causing a collision with B.
+ // Add /project-b to project_a directly. Its key now matches project_b's group.
project_a
.update(cx, |project, cx| {
project.find_or_create_worktree("/project-b", true, cx)
@@ -3162,241 +2787,19 @@ async fn test_worktree_collision_keeps_active_workspace(cx: &mut TestAppContext)
.expect("should add worktree");
cx.run_until_parked();
- // Workspace A (the incoming duplicate) should have been dropped.
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- !workspace_ids.contains(&workspace_a.entity_id()),
- "workspace A should have been dropped"
- );
- });
-
- // The active workspace should still be B.
- assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspace().entity_id()),
- workspace_b.entity_id(),
- "workspace B should still be active"
- );
-
- // The linked worktree sibling should have migrated into B's group
- // (it got the folder add and now shares the same key).
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- workspace_ids.contains(&workspace_wt.entity_id()),
- "linked worktree workspace should still exist"
- );
+ // Both workspaces should now be in one group.
+ multi_workspace.read_with(cx, |mw, _| {
assert_eq!(
- mw.project_group_keys().count(),
+ mw.project_groups().len(),
1,
- "should have 1 group after merge"
+ "should have merged into 1 group"
);
assert_eq!(
- mw.workspaces().count(),
+ mw.project_groups()[0].workspaces.len(),
2,
- "should have 2 workspaces (B + linked worktree)"
+ "both workspaces in the merged group"
);
});
-
- // The linked worktree workspace should have gotten the new folder.
- let wt_worktree_count =
- project_wt.read_with(cx, |project, cx| project.visible_worktrees(cx).count());
- assert_eq!(
- wt_worktree_count, 2,
- "linked worktree project should have gotten /project-b"
- );
-
- // After: everything merged under one group. Thread A migrated,
- // worktree thread shows its chip, B's thread and draft remain.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread A",
- " Worktree Thread {project-a:wt-feature}",
- " Thread B",
- ]
- );
-}
-
-#[gpui::test]
-async fn test_worktree_add_syncs_linked_worktree_sibling(cx: &mut TestAppContext) {
- // When a worktree is added to the main workspace, a linked worktree
- // sibling (different root paths, same project group key) should also
- // get the new folder added to its project.
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- fs.insert_tree(
- "/project",
- serde_json::json!({
- ".git": {
- "worktrees": {
- "feature": {
- "commondir": "../../",
- "HEAD": "ref: refs/heads/feature",
- },
- },
- },
- "src": {},
- }),
- )
- .await;
-
- fs.insert_tree(
- "/wt-feature",
- serde_json::json!({
- ".git": "gitdir: /project/.git/worktrees/feature",
- "src": {},
- }),
- )
- .await;
-
- fs.add_linked_worktree_for_repo(
- Path::new("/project/.git"),
- false,
- git::repository::Worktree {
- path: PathBuf::from("/wt-feature"),
- ref_name: Some("refs/heads/feature".into()),
- sha: "aaa".into(),
- is_main: false,
- },
- )
- .await;
-
- // Create a second independent project to add as a folder later.
- fs.insert_tree(
- "/other-project",
- serde_json::json!({ ".git": {}, "src": {} }),
- )
- .await;
-
- cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
-
- let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
- let worktree_project = project::Project::test(fs.clone(), ["/wt-feature".as_ref()], cx).await;
-
- main_project
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
- worktree_project
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
-
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Add agent panel to the main workspace.
- let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
- add_agent_panel(&main_workspace, cx);
-
- // Open the linked worktree as a separate workspace.
- let wt_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(worktree_project.clone(), window, cx)
- });
- add_agent_panel(&wt_workspace, cx);
- cx.run_until_parked();
-
- // Both workspaces should share the same project group key [/project].
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(
- mw.project_group_keys().count(),
- 1,
- "should have 1 project group key before add"
- );
- assert_eq!(mw.workspaces().count(), 2, "should have 2 workspaces");
- });
-
- // Save threads against each workspace.
- save_named_thread_metadata("main-thread", "Main Thread", &main_project, cx).await;
- save_named_thread_metadata("wt-thread", "Worktree Thread", &worktree_project, cx).await;
-
- // Verify both threads are under the old key [/project].
- let old_key_paths = PathList::new(&[PathBuf::from("/project")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key before add"
- );
- });
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project]",
- " Worktree Thread {wt-feature}",
- " Main Thread",
- ]
- );
-
- // Add /other-project as a folder to the main workspace.
- main_project
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/other-project", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // The linked worktree workspace should have gotten the new folder too.
- let wt_worktree_count =
- worktree_project.read_with(cx, |project, cx| project.visible_worktrees(cx).count());
- assert_eq!(
- wt_worktree_count, 2,
- "linked worktree project should have gotten the new folder"
- );
-
- // Both workspaces should still exist under one key.
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(mw.workspaces().count(), 2, "both workspaces should survive");
- assert_eq!(
- mw.project_group_keys().count(),
- 1,
- "should still have 1 project group key"
- );
- });
-
- // Threads should have been migrated to the new key.
- let new_key_paths =
- PathList::new(&[PathBuf::from("/other-project"), PathBuf::from("/project")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 0,
- "should have 0 threads under old key after migration"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 2,
- "should have 2 threads under new key after migration"
- );
- });
-
- // Both threads should still be visible in the sidebar.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [other-project, project]",
- " Worktree Thread {project:wt-feature}",
- " Main Thread",
- ]
- );
}
#[gpui::test]