diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9ac7e72ffe..63fd59628c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1490,6 +1490,8 @@ impl Database { visible: db_worktree.visible, updated_entries: Default::default(), removed_entries: Default::default(), + updated_repositories: Default::default(), + removed_repositories: Default::default(), diagnostic_summaries: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, @@ -1499,38 +1501,77 @@ impl Database { .worktrees .iter() .find(|worktree| worktree.id == db_worktree.id as u64); - let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { - worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_entry::Column::IsDeleted.eq(false) - }; - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) - .add(entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if db_entry.is_deleted { - worktree.removed_entries.push(db_entry.id as u64); + // File entries + { + let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { + worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) } else { - worktree.updated_entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - }); + worktree_entry::Column::IsDeleted.eq(false) + }; + + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) + .add(entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if db_entry.is_deleted { + worktree.removed_entries.push(db_entry.id as u64); + } else { + worktree.updated_entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + }); + } + } + } + + // Repository Entries + { + let repository_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_repository::Column::IsDeleted.eq(false) + }; + + let mut db_repositories = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) + .add(repository_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_repository) = db_repositories.next().await { + let db_repository = db_repository?; + if db_repository.is_deleted { + worktree + .removed_repositories + .push(db_repository.dot_git_entry_id as u64); + } else { + worktree.updated_repositories.push(proto::RepositoryEntry { + dot_git_entry_id: db_repository.dot_git_entry_id as u64, + scan_id: db_repository.scan_id as u64, + work_directory: db_repository.work_directory_path, + branch: db_repository.branch, + }); + } } } @@ -2555,6 +2596,7 @@ impl Database { root_name: db_worktree.root_name, visible: db_worktree.visible, entries: Default::default(), + repository_entries: Default::default(), diagnostic_summaries: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, @@ -2592,6 +2634,31 @@ impl Database { } } + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project_id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = + worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.push(proto::RepositoryEntry { + dot_git_entry_id: db_repository_entry.dot_git_entry_id as u64, + scan_id: db_repository_entry.scan_id as u64, + work_directory: db_repository_entry.work_directory_path, + branch: db_repository_entry.branch, + }); + } + } + } + // Populate worktree diagnostic summaries. { let mut db_summaries = worktree_diagnostic_summary::Entity::find() @@ -3273,6 +3340,8 @@ pub struct RejoinedWorktree { pub visible: bool, pub updated_entries: Vec, pub removed_entries: Vec, + pub updated_repositories: Vec, + pub removed_repositories: Vec, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, @@ -3327,6 +3396,7 @@ pub struct Worktree { pub root_name: String, pub visible: bool, pub entries: Vec, + pub repository_entries: Vec, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0d57582d7e..39f8266978 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1386,9 +1386,8 @@ async fn join_project( removed_entries: Default::default(), scan_id: worktree.scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id, - // TODO repo - updated_repositories: vec![], - removed_repositories: vec![], + updated_repositories: worktree.repository_entries, + removed_repositories: Default::default(), }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { session.peer.send(session.connection_id, update.clone())?; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 00273722c4..c90354f214 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -785,6 +785,28 @@ async fn apply_client_operation( } client.fs.set_index_for_repo(&dot_git_dir, &contents).await; } + + ClientOperation::WriteGitBranch { + repo_path, + new_branch, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + log::info!( + "{}: writing git branch for repo {:?}: {:?}", + client.username, + repo_path, + new_branch + ); + + let dot_git_dir = repo_path.join(".git"); + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + client.fs.set_branch_name(&dot_git_dir, new_branch).await; + } } Ok(()) } @@ -859,6 +881,12 @@ fn check_consistency_between_clients(clients: &[(Rc, TestAppContext) host_snapshot.abs_path(), guest_project.remote_id(), ); + assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), + "{} has different repositories than the host for worktree {:?} and project {:?}", + client.username, + host_snapshot.abs_path(), + guest_project.remote_id(), + ); assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(), "{} has different scan id than the host for worktree {:?} and project {:?}", client.username, @@ -1151,6 +1179,10 @@ enum ClientOperation { repo_path: PathBuf, contents: Vec<(PathBuf, String)>, }, + WriteGitBranch { + repo_path: PathBuf, + new_branch: Option, + }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1664,7 +1696,7 @@ impl TestPlan { } // Update a git index - 91..=95 => { + 91..=93 => { let repo_path = client .fs .directories() @@ -1698,6 +1730,24 @@ impl TestPlan { }; } + // Update a git branch + 94..=95 => { + let repo_path = client + .fs + .directories() + .choose(&mut self.rng) + .unwrap() + .clone(); + + let new_branch = (self.rng.gen_range(0..10) > 3) + .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); + + break ClientOperation::WriteGitBranch { + repo_path, + new_branch, + }; + } + // Create or update a file or directory 96.. => { let is_dir = self.rng.gen::(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 3c550aa6bb..ea26a969d2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -117,13 +117,10 @@ pub struct Snapshot { completed_scan_id: usize, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct RepositoryEntry { pub(crate) scan_id: usize, - pub(crate) dot_git_entry_id: ProjectEntryId, - /// Relative to the worktree, the repository for the root will have - /// a work directory equal to: "" - pub(crate) work_directory: RepositoryWorkDirectory, + pub(crate) work_directory_id: ProjectEntryId, pub(crate) branch: Option>, } @@ -132,17 +129,16 @@ impl RepositoryEntry { self.branch.clone() } - pub fn work_directory(&self) -> Arc { - self.work_directory.0.clone() + pub fn work_directory_id(&self) -> ProjectEntryId { + self.work_directory_id } } impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { - dot_git_entry_id: value.dot_git_entry_id.to_proto(), scan_id: value.scan_id as u64, - work_directory: value.work_directory.to_string_lossy().to_string(), + work_directory_id: value.work_directory_id.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } } @@ -212,6 +208,7 @@ impl AsRef for RepositoryWorkDirectory { pub struct LocalSnapshot { ignores_by_parent_abs_path: HashMap, (Arc, usize)>, // The ProjectEntryId corresponds to the entry for the .git dir + // work_directory_id git_repositories: TreeMap, removed_entry_ids: HashMap, next_entry_id: Arc, @@ -701,12 +698,12 @@ impl LocalWorktree { ) { for a_repo in a { let matched = b.find(|b_repo| { - a_repo.dot_git_entry_id == b_repo.dot_git_entry_id + a_repo.work_directory_id == b_repo.work_directory_id && a_repo.scan_id == b_repo.scan_id }); if matched.is_none() { - updated.insert(a_repo.dot_git_entry_id, a_repo.clone()); + updated.insert(a_repo.work_directory_id, a_repo.clone()); } } } @@ -1158,7 +1155,7 @@ impl LocalWorktree { repo_path: RepoPath, cx: &mut ModelContext, ) -> Task> { - let Some(git_ptr) = self.git_repositories.get(&repo.dot_git_entry_id).map(|git_ptr| git_ptr.to_owned()) else { + let Some(git_ptr) = self.git_repositories.get(&repo.work_directory_id).map(|git_ptr| git_ptr.to_owned()) else { return Task::Ready(Some(None)) }; let git_ptr = git_ptr.repo_ptr; @@ -1476,6 +1473,10 @@ impl Snapshot { self.traverse_from_offset(true, include_ignored, 0) } + pub fn repositories(&self) -> impl Iterator { + self.repository_entries.values() + } + pub fn paths(&self) -> impl Iterator> { let empty_path = Path::new(""); self.entries_by_path diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 37092dea4d..315352b01e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -982,10 +982,9 @@ message Entry { } message RepositoryEntry { - uint64 dot_git_entry_id = 1; - uint64 scan_id = 2; - string work_directory = 4; - optional string branch = 5; + uint64 scan_id = 1; + uint64 work_directory_id = 2; + optional string branch = 3; } message BufferState {