diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 2a87d617ebb19117ca87c00cc0887b07e416c8bd..75175372f24a83cfb50e8f87deae93e3f03e1a8a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -65,6 +65,7 @@ CREATE TABLE "worktrees" ( "scan_id" INTEGER NOT NULL, "is_complete" BOOL NOT NULL DEFAULT FALSE, "completed_scan_id" INTEGER NOT NULL, + "root_repo_common_dir" VARCHAR, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20251208000000_test_schema.sql b/crates/collab/migrations/20251208000000_test_schema.sql index 8a56b9ce982f9a39a14bfc55fe8a34870ddea1c6..0110dd149b1143a3edcf76a1e0b18fbf1a22287c 100644 --- a/crates/collab/migrations/20251208000000_test_schema.sql +++ b/crates/collab/migrations/20251208000000_test_schema.sql @@ -484,7 +484,8 @@ CREATE TABLE public.worktrees ( visible boolean NOT NULL, scan_id bigint NOT NULL, is_complete boolean DEFAULT false NOT NULL, - completed_scan_id bigint + completed_scan_id bigint, + root_repo_common_dir character varying ); ALTER TABLE ONLY public.breakpoints ALTER COLUMN id SET DEFAULT nextval('public.breakpoints_id_seq'::regclass); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 3e4c36631b29d35871cac101542bcc6904fbb271..44abc37af66e3f169d3af1a7d5e29063e382c620 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -559,6 +559,7 @@ pub struct RejoinedWorktree { pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, + pub root_repo_common_dir: Option, } pub struct LeftRoom { @@ -638,6 +639,7 @@ pub struct Worktree { pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, + pub root_repo_common_dir: Option, } #[derive(Debug)] diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 3fc59f96332180d7d7bca4b6f71a345d9699e9e2..b1ea638072a30d6b881a711448223449aa9f53e2 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -87,6 +87,7 @@ impl Database { visible: ActiveValue::set(worktree.visible), scan_id: ActiveValue::set(0), completed_scan_id: ActiveValue::set(0), + root_repo_common_dir: ActiveValue::set(None), } })) .exec(&*tx) @@ -203,6 +204,7 @@ impl Database { visible: ActiveValue::set(worktree.visible), scan_id: ActiveValue::set(0), completed_scan_id: ActiveValue::set(0), + root_repo_common_dir: ActiveValue::set(None), })) .on_conflict( OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) @@ -266,6 +268,7 @@ impl Database { ActiveValue::default() }, abs_path: ActiveValue::set(update.abs_path.clone()), + root_repo_common_dir: ActiveValue::set(update.root_repo_common_dir.clone()), ..Default::default() }) .exec(&*tx) @@ -761,6 +764,7 @@ impl Database { settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, + root_repo_common_dir: db_worktree.root_repo_common_dir, legacy_repository_entries: Default::default(), }, ) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 3197d142cba7a1969e6fdb9423dc94497f6ca53c..94e003fd2d27c97a53f66606d11ed2e15609b728 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -629,6 +629,7 @@ impl Database { settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, + root_repo_common_dir: db_worktree.root_repo_common_dir, }; let rejoined_worktree = rejoined_project diff --git a/crates/collab/src/db/tables/worktree.rs b/crates/collab/src/db/tables/worktree.rs index 46d9877dff152cdc3b30531606febec65595fec1..f67a9749a48e51fce81f97ad2faf8609c50a0204 100644 --- a/crates/collab/src/db/tables/worktree.rs +++ b/crates/collab/src/db/tables/worktree.rs @@ -15,6 +15,7 @@ pub struct Model { pub scan_id: i64, /// The last scan that fully completed. pub completed_scan_id: i64, + pub root_repo_common_dir: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e05df1909db1e8afed0c06425d84799ff985f3c5..7ed488b0ba62c10326a0e2154f0d2ba895e20a4f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1485,6 +1485,7 @@ fn notify_rejoined_projects( worktree_id: worktree.id, abs_path: worktree.abs_path.clone(), root_name: worktree.root_name, + root_repo_common_dir: worktree.root_repo_common_dir, updated_entries: worktree.updated_entries, removed_entries: worktree.removed_entries, scan_id: worktree.scan_id, @@ -1943,6 +1944,7 @@ async fn join_project( worktree_id, abs_path: worktree.abs_path.clone(), root_name: worktree.root_name, + root_repo_common_dir: worktree.root_repo_common_dir, updated_entries: worktree.entries, removed_entries: Default::default(), scan_id: worktree.scan_id, diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index a64233caba014aa49bd64f98634b40abeef88e8e..fdaacd768444bd44d8414247f922f38afb7e81d5 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::{self, Path, PathBuf}; use call::ActiveCall; use client::RECEIVE_TIMEOUT; @@ -17,6 +17,61 @@ use workspace::{MultiWorkspace, Workspace}; use crate::TestServer; +#[gpui::test] +async fn test_root_repo_common_dir_sync( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // Set up a project whose root IS a git repository. + client_a + .fs() + .insert_tree( + path!("/project"), + json!({ ".git": {}, "file.txt": "content" }), + ) + .await; + + let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await; + executor.run_until_parked(); + + // Host should see root_repo_common_dir pointing to .git at the root. + let host_common_dir = project_a.read_with(cx_a, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + worktree.read(cx).snapshot().root_repo_common_dir().cloned() + }); + assert_eq!( + host_common_dir.as_deref(), + Some(path::Path::new(path!("/project/.git"))), + ); + + // Share the project and have client B join. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + executor.run_until_parked(); + + // Guest should see the same root_repo_common_dir as the host. + let guest_common_dir = project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + worktree.read(cx).snapshot().root_repo_common_dir().cloned() + }); + assert_eq!( + guest_common_dir, host_common_dir, + "guest should see the same root_repo_common_dir as host", + ); +} + fn collect_diff_stats( panel: &gpui::Entity, cx: &C, diff --git a/crates/edit_prediction/src/license_detection.rs b/crates/edit_prediction/src/license_detection.rs index 55635bcfd04cb6288f44907da051fa1f33d41922..88edfc306ebca21076908b3c05f7cf2837b19209 100644 --- a/crates/edit_prediction/src/license_detection.rs +++ b/crates/edit_prediction/src/license_detection.rs @@ -319,6 +319,7 @@ impl LicenseDetectionWatcher { } worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) + | worktree::Event::UpdatedRootRepoCommonDir | worktree::Event::Deleted => {} }); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 286d3a85f86173bff5d17d8d7c86d26464a04714..2f579f5a724db143bbd4b0f9853a217bd6b14655 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -4414,7 +4414,8 @@ impl LspStore { } worktree::Event::UpdatedGitRepositories(_) | worktree::Event::DeletedEntry(_) - | worktree::Event::Deleted => {} + | worktree::Event::Deleted + | worktree::Event::UpdatedRootRepoCommonDir => {} }) .detach() } diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 1ae5b0e809f3803c3f8858afb065637ba0a0f256..fb1b7e96e4a20370493e0837360a28583ffbbfc0 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -59,7 +59,7 @@ impl WorktreeRoots { let path = TriePath::from(entry.path.as_ref()); this.roots.remove(&path); } - WorktreeEvent::Deleted => {} + WorktreeEvent::Deleted | WorktreeEvent::UpdatedRootRepoCommonDir => {} } }), }) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 92f7db453a81c6224455002b7811f2e6945f2a82..ca448ce53118fd23fec0dfc920ee67f5d6d19c41 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -812,6 +812,7 @@ impl WorktreeStore { // The worktree root itself has been deleted (for single-file worktrees) // The worktree will be removed via the observe_release callback } + worktree::Event::UpdatedRootRepoCommonDir => {} } }) .detach(); diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto index aa964c64cd04db71a71ac081e034be10cbf95048..71351fb74c5834fe0b1650f22e851c21cd752466 100644 --- a/crates/proto/proto/call.proto +++ b/crates/proto/proto/call.proto @@ -225,6 +225,7 @@ message UpdateWorktree { uint64 scan_id = 8; bool is_last_update = 9; string abs_path = 10; + optional string root_repo_common_dir = 11; } // deprecated diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index c21934338f97cc8ed3e04b917c7db84fccecd031..dd77d2a2da8d4dbc2c0f91f63cb59dd1591ee3f4 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -881,6 +881,7 @@ pub fn split_worktree_update(mut message: UpdateWorktree) -> impl Iterator> = Rc::new(Cell::new(0)); + tree.update(cx, { + let event_count = event_count.clone(); + |_, cx| { + cx.subscribe(&cx.entity(), move |_, _, event, _| { + if matches!(event, Event::UpdatedRootRepoCommonDir) { + event_count.set(event_count.get() + 1); + } + }) + .detach(); + } + }); + + // Remove .git — root_repo_common_dir should become None. + fs.remove_file( + &PathBuf::from(path!("/linked_worktree/.git")), + Default::default(), + ) + .await + .unwrap(); + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + assert_eq!(tree.snapshot().root_repo_common_dir(), None); + }); + assert_eq!( + event_count.get(), + 1, + "should have emitted UpdatedRootRepoCommonDir on removal" + ); +} + fn init_test(cx: &mut gpui::TestAppContext) { zlog::init_test();